Merge "Fix flapping DockerServiceWarning alert"
diff --git a/.kitchen.travis.yml b/.kitchen.travis.yml
deleted file mode 100644
index f847543..0000000
--- a/.kitchen.travis.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-suites:
-
-  - name: <%= ENV['SUITE'] %>
-    provisioner:
-      pillars-from-files:
-        neutron.sls: tests/pillar/<%= ENV['SUITE'] %>.sls
diff --git a/.travis.yml b/.travis.yml
index fa7c5f9..64743ee 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,15 +17,16 @@
   - bundle install
 
 env:
-  - PLATFORM=trevorj/salty-whales:xenial SUITE=client_compose
-  - PLATFORM=trevorj/salty-whales:xenial SUITE=client_container
-  - PLATFORM=trevorj/salty-whales:xenial SUITE=host_single
+  - PLATFORM=trevorj/salty-whales:xenial SUITE=client-compose
+  - PLATFORM=trevorj/salty-whales:xenial SUITE=client-container
+  - PLATFORM=trevorj/salty-whales:xenial SUITE=host-single
 
 before_script:
   - make test | tail
 
 script:
-  - KITCHEN_LOCAL_YAML=.kitchen.travis.yml bundle exec kitchen test -t tests/integration
+  - test ! -e .kitchen.yml || bundle exec kitchen converge ${SUITE} || true
+  - test ! -e .kitchen.yml || bundle exec kitchen verify ${SUITE} -t tests/integration
 
 notifications:
   webhooks:
diff --git a/README.rst b/README.rst
index 81cb038..4cbdb11 100644
--- a/README.rst
+++ b/README.rst
@@ -279,6 +279,65 @@
             password: password2
 
 
+Docker container service management
+-----------------------------------
+
+Enforce the service in container is started
+
+.. code-block:: yaml
+
+    contrail_control_started:
+      dockerng_service.start:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+or
+
+.. code-block:: yaml
+
+    contrail_control_started:
+      dockerng_service.start:
+        - container: contrail_controller
+        - service: contrail-control
+
+
+Enforce the service in container is stoped
+
+.. code-block:: yaml
+
+    contrail_control_stoped:
+      dockerng_service.stop:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+Enforce the service in container will be restarted
+
+.. code-block:: yaml
+
+    contrail_control_restart:
+      dockerng_service.restart:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+Enforce the service in container is enabled
+
+.. code-block:: yaml
+
+    contrail_control_enable:
+      dockerng_service.enable:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+Enforce the service in container is disabled
+
+.. code-block:: yaml
+
+    contrail_control_disable:
+      dockerng_service.disable:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+
 More Information
 ================
 
diff --git a/_modules/dockerng_service.py b/_modules/dockerng_service.py
new file mode 100644
index 0000000..081b7e0
--- /dev/null
+++ b/_modules/dockerng_service.py
@@ -0,0 +1,99 @@
+#!/usr/bin/python
+# Copyright 2017 Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+try:
+    import docker
+    HAS_DOCKER = True
+except ImportError:
+    HAS_DOCKER = False
+
+__opts__ = {}
+__virtualname__ = 'dockerng_service'
+
+
+def __virtual__():
+    '''
+    Only load this module if docker library is installed.
+    '''
+    if HAS_DOCKER:
+        return __virtualname__
+    return (False, 'dockerio execution module not loaded: docker python library not available.')
+
+
+def status(container, service):
+    cmd = "systemctl show " + service + " -p ActiveState,SubState,UnitFileState"
+    data =  __salt__['dockerng.run'](container, cmd)
+    data = data.splitlines()
+    result = dict(s.split('=') for s in data)
+    return result
+
+
+def status_retcode(container, service):
+    cmd = "systemctl show " + service + " -p ActiveState,SubState,UnitFileState"
+    data =  __salt__['dockerng.run'](container, cmd)
+    data = data.splitlines()
+    result = dict(s.split('=') for s in data)
+    if result['ActiveState'] == "active" and result['SubState'] == "running":
+        return True
+    return False
+
+
+def restart(container, service):
+    cmd = "systemctl restart " + service
+    data =  __salt__['dockerng.run'](container, cmd)
+    if len(data) > 0:
+        return False
+    return True
+
+
+def stop(container, service):
+    cmd = "systemctl stop " + service
+    data =  __salt__['dockerng.run'](container, cmd)
+    if len(data) > 0:
+        return False
+    return True
+
+
+def start(container, service):
+    cmd = "systemctl start " + service
+    data =  __salt__['dockerng.run'](container, cmd)
+    if len(data) > 0:
+        return False
+    return True
+
+
+def enable(container, service):
+    cmd = "systemctl enable " + service
+    data =  __salt__['dockerng.run'](container, cmd)
+    if len(data) > 0:
+        return False
+    return True
+
+
+def reload(container, service):
+    cmd = "systemctl reload " + service
+    data =  __salt__['dockerng.run'](container, cmd)
+    if len(data) > 0:
+        return False
+    return True
+
+
+def disable(container, service):
+    cmd = "systemctl disable " + service
+    data =  __salt__['dockerng.run'](container, cmd)
+    if len(data) > 0:
+        return False
+    return True
diff --git a/_states/dockerng_service.py b/_states/dockerng_service.py
new file mode 100644
index 0000000..ed13798
--- /dev/null
+++ b/_states/dockerng_service.py
@@ -0,0 +1,297 @@
+#!/usr/bin/python
+# Copyright 2017 Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+'''
+Management of Contrail resources
+================================
+
+:depends:   - vnc_api Python module
+
+
+Enforce the service in container is running
+-------------------------------------------
+
+.. code-block:: yaml
+
+    contrail_control_running:
+      dockerng_service.running:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+or
+
+.. code-block:: yaml
+
+    contrail_control_running:
+      dockerng_service.running:
+        - container: contrail_controller
+        - service: contrail-control
+
+
+Enforce the service in container is dead
+------------------------------------------
+
+.. code-block:: yaml
+
+    contrail_control_dead:
+      dockerng_service.dead:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+Enforce the service in container will be restarted
+--------------------------------------------------
+
+.. code-block:: yaml
+
+    contrail_control_restarted:
+      dockerng_service.restarted:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+Enforce the service in container is enabled
+-------------------------------------------
+
+.. code-block:: yaml
+
+    contrail_control_enabled:
+      dockerng_service.enabled:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+Enforce the service in container is disabled
+--------------------------------------------
+
+.. code-block:: yaml
+
+    contrail_control_disabled:
+      dockerng_service.disabled:
+        - container: f020d0d3efa8
+        - service: contrail-control
+
+'''
+
+
+def __virtual__():
+    '''
+    Load Contrail module
+    '''
+    return 'dockerng_service'
+
+
+def running(container, service=None, services=None, **kwargs):
+    '''
+    Ensures that the service in the container is running
+
+    :param container:    ID or name of the container
+    :param services:     List of services
+    :param service:      Service name
+    '''
+    ret = {'name': kwargs.get('name', 'dockerng_service.running'),
+           'changes': {},
+           'result': True,
+           'comment': {}
+           }
+
+    if service and not services:
+        services = [service, ]
+
+    for service in services:
+        status = __salt__['dockerng_service.status'](container, service)
+
+        if status['ActiveState'] != "active" and status['SubState'] != "running":
+            if __opts__['test']:
+                ret['result'] = None
+                ret['comment'][service] = " will be started"
+            else:
+                __salt__['dockerng_service.start'](container, service)
+                ret['comment'] = service + " in  " + container + " has been started"
+                ret['changes'] = {service:  "started"}
+
+    return ret
+
+
+def dead(container, service, **kwargs):
+    '''
+    Ensures that the service in the container is dead
+
+    :param container:    ID or name of the container
+    :param service:      Service name
+    '''
+    ret = {'name': service + " in " + container,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    status = __salt__['dockerng_service.status'](container, service)
+
+    if status['ActiveState'] != "inactive" and status['SubState'] != "dead":
+        if __opts__['test']:
+            ret['result'] = None
+            ret['comment'] = service + " in  " + container + " will be stoped"
+            return ret
+
+        __salt__['dockerng_service.stop'](container, service)
+        ret['comment'] = service + " in  " + container + " has been stoped"
+        ret['changes'] = {"new": "stoped", "old": "started"}
+        return ret
+
+    return ret
+
+
+def restarted(container, service, **kwargs):
+    '''
+    Service in the container will be restarted
+
+    :param container:    ID or name of the container
+    :param service:      Service name
+    '''
+    ret = {'name': service + " in " + container,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    if __opts__['test']:
+        ret['result'] = None
+        ret['comment'] = service + " in  " + container + " will be restarted"
+        return ret
+
+    res = __salt__['dockerng_service.restart'](container, service)
+    ret['comment'] = service + " in  " + container + " has been restarted"
+    ret['changes'] = {"status": "restarted"}
+    return ret
+
+
+def enabled(container, service, **kwargs):
+    '''
+    Ensures that the service in the container is enabled
+
+    :param container:    ID or name of the container
+    :param service:      Service name
+    '''
+    ret = {'name': service + " in " + container,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    status = __salt__['dockerng_service.status'](container, service)
+
+    if status['UnitFileState'] != "enabled":
+        if __opts__['test']:
+            ret['result'] = None
+            ret['comment'] = service + " in  " + container + " will be enabled"
+            return ret
+
+        __salt__['dockerng_service.enable'](container, service)
+        ret['comment'] = service + " in  " + container + " has been enabled"
+        ret['changes'] = {"new": "enabled", "old": "disabled"}
+        return ret
+
+    return ret
+
+
+def disabled(container, service, **kwargs):
+    '''
+    Ensures that the service in the container is disabled
+
+    :param container:    ID or name of the container
+    :param service:      Service name
+    '''
+    ret = {'name': service + " in " + container,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    status = __salt__['dockerng_service.status'](container, service)
+
+    if status['UnitFileState'] != "disabled":
+        if __opts__['test']:
+            ret['result'] = None
+            ret['comment'] = service + " in  " + container + " will be disabled"
+            return ret
+
+        __salt__['dockerng_service.disable'](container, service)
+        ret['comment'] = service + " in  " + container + " has been disabled"
+        ret['changes'] = {"old": "enabled", "new": "disabled"}
+        return ret
+
+    return ret
+
+
+def mod_watch(name,
+              contrainer=None,
+              sfun=None,
+              sig=None,
+              reload=False,
+              full_restart=False,
+              init_delay=None,
+              force=False,
+              **kwargs):
+    '''
+    The service watcher, called to invoke the watch command.
+
+    :param name:         The name of the init or rc script used to manage the
+                         service
+    :param sfun:         The original function which triggered the mod_watch
+                         call (`service.running`, for example).
+    :param sig:          The string to search for when looking for the service
+                         process with ps
+    :param reload:       Use reload instead of the default restart (exclusive
+                         option with full_restart, defaults to reload if both
+                         are used)
+    :param full_restart: Use service.full_restart instead of restart
+                         (exclusive option with reload)
+    :param force:        Use service.force_reload instead of reload
+                         (needs reload to be set to True)
+    :param  init_delay:  Add a sleep command (in seconds) before the service is
+                         restarted/reloaded
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': {}}
+
+    service = kwargs.get('service')
+    services = kwargs.get('services')
+    if not services and service:
+        services = [service, ]
+    elif not services and not service:
+        ret['result'] = False
+        ret['comment'] = "Service was not defined"
+        return ret
+
+    container = kwargs.get('container', None)
+    if not container:
+        ret['result'] = False
+        ret['comment'] = "Container was not defined"
+        return ret
+
+    ret['comment'] = {}
+    if sfun == 'running':
+
+        for service in services:
+            status = __salt__['dockerng_service.status'](container, service)
+
+
+            if __opts__['test']:
+                ret['result'] = None
+                ret['comment'][service] = "Services will be restarted"
+                ret['changes'][service] = "will be restarted"
+            else:
+                res = __salt__['dockerng_service.restart'](container, service)
+                ret['comment'] = "Services has been restarted"
+                ret['changes'][service] = "restarted"
+    else:
+        ret['comment'] = 'Unable to trigger watch for dockerng_service.{0}'.format(sfun)
+        ret['result'] = False
+    return ret
diff --git a/debian/control b/debian/control
index d73ee38..d6faf77 100644
--- a/debian/control
+++ b/debian/control
@@ -2,7 +2,7 @@
 Maintainer: Michael Kuty <michael.kuty@tcpcloud.eu>
 Section: admin
 Priority: optional
-Build-Depends: salt-master, python, python-yaml, debhelper (>= 9), salt-master, python, python-yaml
+Build-Depends: salt-master, python, python-yaml, debhelper (>= 9), salt-master, python, python-yaml, salt-formula-linux
 Standards-Version: 3.9.6
 Homepage: http://www.tcpcloud.eu
 Vcs-Browser: https://github.com/tcpcloud/salt-formula-docker
@@ -10,6 +10,6 @@
 
 Package: salt-formula-docker
 Architecture: all
-Depends: ${misc:Depends}, salt-master, reclass
+Depends: ${misc:Depends}
 Description: docker salt formula
  Install and configure docker system.
diff --git a/docker/client/init.sls b/docker/client/init.sls
index 8e0ca58..f628ddc 100644
--- a/docker/client/init.sls
+++ b/docker/client/init.sls
@@ -2,20 +2,22 @@
 {%- if client.get('enabled') %}
 
 include:
-  {%- if client.network is defined %}
+  {%- if pillar.docker.client.network is defined %}
   - docker.client.network
   {%- endif %}
+  {%- if pillar.docker.client.container is defined %}
   - docker.client.container
-  {%- if client.compose is defined %}
+  {%- endif %}
+  {%- if pillar.docker.client.compose is defined %}
   - docker.client.compose
   {%- endif %}
-  {%- if client.stack is defined %}
+  {%- if pillar.docker.client.stack is defined %}
   - docker.client.stack
   {%- endif %}
-  {%- if client.registry is defined %}
+  {%- if pillar.docker.client.registry is defined %}
   - docker.client.registry
   {%- endif %}
-  {%- if client.service is defined %}
+  {%- if pillar.docker.client.service is defined %}
   - docker.client.service
   {%- endif %}
 
diff --git a/docker/client/service.sls b/docker/client/service.sls
index dbe6de8..18a925a 100644
--- a/docker/client/service.sls
+++ b/docker/client/service.sls
@@ -35,8 +35,9 @@
         {%- if service.workdir is defined %} --workdir {{ service.workdir }}{%- endif %}
         {%- if service.mode is defined %} --mode {{ service.mode }}{%- endif %}
         {%- if service.endpoint is defined %} --endpoint-mode {{ service.endpoint }}{%- endif %}
-        {%- if service.constraint is defined %} --constraint {{ service.constraint }}{%- endif %}
         {%- if service.hostname is defined %} --hostname {{ service.hostname }}{%- endif %}
+        {%- if service.constraint is defined %} --constraint {{ service.constraint }}{%- endif %}
+        {%- for constraint in service.get('constraints', []) %} --constraint {{ constraint }}{%- endfor %}
         {%- for name, volume in service.get('volume', {}).iteritems() %} --mount {% for key, value in volume.iteritems() %}{{ key }}={{ value }}{% if not loop.last %},{% endif %}{% endfor %}{%- endfor %}
         {%- for param, value in service.get('restart', {}).iteritems() %} --restart-{{ param }} {{ value }}{%- endfor %}
         {%- for param, value in service.get('update', {}).iteritems() %} --update-{{ param }} {{ value }}{%- endfor %}
diff --git a/docker/client/stack.sls b/docker/client/stack.sls
index 8533f9a..8bf7ae8 100644
--- a/docker/client/stack.sls
+++ b/docker/client/stack.sls
@@ -52,9 +52,11 @@
           {%- set path = volume.split(':')[0] %}
         {%- elif volume is mapping and volume.get('type', 'bind') == 'bind' %}
           {%- set path = volume.source %}
+        {%- else %}
+          {%- set path = None %}
         {%- endif %}
 
-        {%- if path is defined %}
+        {%- if path != None and path not in compose.get('volume', {}).keys() %}
 docker_{{ app }}_{{ name }}_volume_{{ path }}:
   file.directory:
     - name: {{ path }}
diff --git a/docker/files/default b/docker/files/default
index 42f9bfb..db37876 100644
--- a/docker/files/default
+++ b/docker/files/default
@@ -6,7 +6,6 @@
 
 # Use DOCKER_OPTS to modify the daemon startup options.
 #DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4"
-DOCKER_OPTS=" --log-driver={{ host.get('options', {}).get('log-driver','json-file') }} --log-opt max-size={{ host.get('options').get('log-opt', {}).get('max-size', '50m') }}"
 
 # If you need Docker to use an HTTP proxy, it can also be specified here.
 #export http_proxy="http://127.0.0.1:3128/"
diff --git a/docker/files/docker-compose.yml b/docker/files/docker-compose.yml
index f120327..f1685f0 100644
--- a/docker/files/docker-compose.yml
+++ b/docker/files/docker-compose.yml
@@ -1,5 +1,10 @@
 version: '{{ compose.version|default("3") }}'
 
+{%- if compose.config|default({}) %}
+configs:
+  {{ compose.config|yaml(False)|indent(2) }}
+{%- endif %}
+
 services:
   {%- for name, srv in service.iteritems() %}
   {%- set env_file_set = False %}
diff --git a/docker/files/http-proxy.conf b/docker/files/http-proxy.conf
index 13d494d..9ae5c00 100644
--- a/docker/files/http-proxy.conf
+++ b/docker/files/http-proxy.conf
@@ -1,5 +1,11 @@
 {%- from "docker/map.jinja" import host with context -%}
 [Service]
+{%- if host.proxy.get('http') %}
 Environment="HTTP_PROXY={{ host.proxy.http }}"
+{%- endif -%}
+{%- if host.proxy.get('https') %}
 Environment="HTTPS_PROXY={{ host.proxy.https }}"
+{%- endif -%}
+{%- if host.proxy.get('no_proxy') %}
 Environment="NO_PROXY={{ host.proxy.no_proxy|join(',') }}"
+{%- endif -%}
diff --git a/docker/host.sls b/docker/host.sls
index dbd2cc0..60b3cca 100644
--- a/docker/host.sls
+++ b/docker/host.sls
@@ -48,6 +48,8 @@
   - makedirs: True
   - require_in:
     - service: docker_service
+  - watch_in:
+    - service: docker_service
 
 {% else %}
 
@@ -79,10 +81,11 @@
 
 {%- for name,registry in host.registry.iteritems() %}
 
-docker_{{ registry.address }}_login:
+docker_{{ registry.get('address', name) }}_login:
   cmd.run:
-  - name: 'docker login -u {{ registry.user }} -p {{ registry.password }} {{ registry.address }}'
-  - unless: grep {{ registry.address }} /root/.docker/config.json
+  - name: 'docker login -u {{ registry.user }} -p {{ registry.password }}{% if registry.get('address') %} {{ registry.address }}{% endif %}'
+  - user: {{ registry.get('system_user', 'root') }}
+  - unless: grep {{ registry.address|default('https://index.docker.io/v1/') }} {{ salt['user.info'](registry.get('system_user', 'root')).home }}/.docker/config.json
 
 {%- endfor %}
 
diff --git a/docker/meta/fluentd.yml b/docker/meta/fluentd.yml
new file mode 100644
index 0000000..5e41d2a
--- /dev/null
+++ b/docker/meta/fluentd.yml
@@ -0,0 +1,45 @@
+{%- if pillar.get('fluentd', {}).get('agent', {}).get('enabled', False) %}
+{%- set positiondb = pillar.fluentd.agent.dir.positiondb %}
+agent:
+  config:
+    label:
+      docker:
+        input:
+          container:
+            type: tail
+            tag: temp.docker.container.*
+            path: /var/lib/docker/containers/*/*-json.log
+            path_key: log_path
+            pos_file: {{ positiondb }}/docker.container.pos
+            parser:
+              type: json
+              time_format: '%Y-%m-%dT%H:%M:%S.%NZ'
+              keep_time_key: false
+        filter:
+          enrich:
+            tag: 'temp.docker.container.**'
+            type: record_transformer
+            enable_ruby: true
+            remove_keys: log
+            record:
+              - name: severity_label
+                value: INFO
+              - name: Severity
+                value: 6
+              - name: programname
+                value: docker
+              - name: Payload
+                value: ${record['log']}
+        match:
+          cast_service_tag:
+            tag: 'temp.docker.container.**'
+            type: rewrite_tag_filter
+            rule:
+              - name: log_path
+                regexp: '^.*\/(.*)-json\.log$'
+                result: docker.container.$1
+          push_to_default:
+            tag: 'docker.container.*'
+            type: relabel
+            label: default_output
+{%- endif %}
diff --git a/docker/meta/sphinx.yml b/docker/meta/sphinx.yml
index 203eb0c..a12c200 100644
--- a/docker/meta/sphinx.yml
+++ b/docker/meta/sphinx.yml
@@ -41,4 +41,13 @@
             - "{{ name }} (image {{ service.image }})"
             {%- endfor %}
         {%- endif %}
+        {%- if client.get('stack', {}) %}
+        stacks:
+          value:
+            {%- for name, stack in client.stack.iteritems() %}
+            {%- for svc_name, service in stack.service.iteritems() %}
+            - "{{ name }}-{{ svc_name }} (image {{ service.image }})"
+            {%- endfor %}
+            {%- endfor %}
+        {%- endif %}
     {%- endif %}
diff --git a/metadata/service/support.yml b/metadata/service/support.yml
index a1c25d8..2127589 100644
--- a/metadata/service/support.yml
+++ b/metadata/service/support.yml
@@ -1,6 +1,8 @@
 parameters:
   docker:
     _support:
+      fluentd:
+        enabled: true
       telegraf:
         enabled: true
       collectd:
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
index 3f42101..9451611 100755
--- a/tests/run_tests.sh
+++ b/tests/run_tests.sh
@@ -110,7 +110,7 @@
 }
 
 salt_run() {
-    [ -e ${VEN_DIR}/bin/activate ] && source ${VENV_DIR}/bin/activate
+    [ -e ${VENV_DIR}/bin/activate ] && source ${VENV_DIR}/bin/activate
     salt-call ${SALT_OPTS} $*
 }