diff --git a/devstack/gate_hook.sh b/devstack/gate_hook.sh index aaaa5be8d..b2ba47341 100755 --- a/devstack/gate_hook.sh +++ b/devstack/gate_hook.sh @@ -18,14 +18,4 @@ # Keep all devstack settings here instead of project-config for easy # maintain if we want to change devstack config settings in future. -# Notes(eliqiao): Overwrite defaut ENABLED_SERVICES since currently zun -# doesn't relay on any other OpenStack service yet. - -# Fixme(eliqiao): We don't need nova service, but devstack won't create -# userrc for us if nova service is not enabled, check -# https://github.com/openstack-dev/devstack/blob/master/stack.sh#L1310 - -OVERRIDE_ENABLED_SERVICES="dstat,key,mysql,rabbit,n-api,n-cond,n-cpu,n-crt,n-obj,n-sch,g-api,g-reg,tempest" -export OVERRIDE_ENABLED_SERVICES - $BASE/new/devstack-gate/devstack-vm-gate.sh diff --git a/devstack/lib/nova b/devstack/lib/nova new file mode 100644 index 000000000..7b937fe3d --- /dev/null +++ b/devstack/lib/nova @@ -0,0 +1,60 @@ +#!/bin/bash +# +# lib/nova +# Configure the docker hypervisor + +# Dependencies: +# +# - ``functions`` file +# - ``DEST``, ``NOVA_CONF``, ``STACK_USER`` must be defined + +# ``stack.sh`` calls the entry points in this order: +# +# - configure_nova_docker + +# Save trace setting +XTRACE=$(set +o | grep xtrace) +set +o xtrace + +# Defaults +# -------- +NOVA_CONF_DIR=${NOVA_CONF_DIR:-/etc/nova} +NOVA_CONF=${NOVA_CONF:-NOVA_CONF_DIR/nova.conf} + + +# Entry Points +# ------------ + +# configure_nova_docker - Set config files, create data dirs, etc +function configure_nova_docker { + iniset $NOVA_CONF DEFAULT compute_driver docker.DockerDriver + + # CentOS/RedHat distros don't start the services just after the package + # is installed if it is not explicitily set. So the script fails on + # them in this killall because there is nothing to kill. + sudo killall docker || true + + # Enable debug level logging + if [ -f "/etc/default/docker" ]; then + sudo cat /etc/default/docker + sudo sed -i 's/^.*DOCKER_OPTS=.*$/DOCKER_OPTS=\"--debug --storage-opt dm.override_udev_sync_check=true\"/' /etc/default/docker + sudo cat /etc/default/docker + fi + if [ -f "/etc/sysconfig/docker" ]; then + sudo cat /etc/sysconfig/docker + sudo sed -i 's/^.*OPTIONS=.*$/OPTIONS=--debug --selinux-enabled/' /etc/sysconfig/docker + sudo cat /etc/sysconfig/docker + fi + if [ -f "/usr/lib/systemd/system/docker.service" ]; then + sudo cat /usr/lib/systemd/system/docker.service + sudo sed -i 's/docker daemon/docker daemon --debug/' /usr/lib/systemd/system/docker.service + sudo cat /usr/lib/systemd/system/docker.service + sudo systemctl daemon-reload + fi + + sudo service docker start || true + + # setup rootwrap filters + local rootwrap_conf_src_dir="$DEST/zun/etc/nova" + sudo install -o root -g root -m 644 $rootwrap_conf_src_dir/rootwrap.d/*.filters /etc/nova/rootwrap.d +} diff --git a/devstack/lib/zun b/devstack/lib/zun index f356b1ea5..6a26c07e4 100644 --- a/devstack/lib/zun +++ b/devstack/lib/zun @@ -65,7 +65,7 @@ else fi DOCKER_GROUP=${DOCKER_GROUP:-docker} -ZUN_DRIVER=${DEFAULT_ZUN_DRIVER:-docker} +ZUN_DRIVER=${ZUN_DRIVER:-docker} ETCD_VERSION=v3.0.13 if is_ubuntu; then @@ -83,16 +83,45 @@ function check_docker { fi } +function install_docker { + check_docker || curl -fsSL https://get.docker.com/ | sudo sh + + echo "Adding ${STACK_USER} to ${docker_group}..." + add_user_to_group $STACK_USER $DOCKER_GROUP + echo "Adding $(whoami) to ${DOCKER_GROUP}..." + add_user_to_group $(whoami) $DOCKER_GROUP + + if is_fedora; then + install_package socat dnsmasq + fi + + if is_ubuntu && [ $UBUNTU_RELEASE_BASE_NUM -le 14 ]; then + sudo service docker start || true + else + sudo systemctl enable docker.service + sudo systemctl start docker || true + fi +} + # Test if any zun services are enabled # is_zun_enabled function is_zun_enabled { [[ ,${ENABLED_SERVICES} =~ ,"zun-" ]] && return 0 return 1 } + # cleanup_zun() - Remove residual data files, anything left over from previous # runs that a clean run would need to clean up function cleanup_zun { sudo rm -rf $ZUN_STATE_PATH $ZUN_AUTH_CACHE_DIR + + # Destroy old containers + local container_name_prefix=${CONTAINER_NAME_PREFIX:-zun-} + local containers + containers=`sudo docker ps -a | grep $container_name_prefix | sed "s/.*\($container_name_prefix[0-9a-zA-Z-]*\).*/\1/g"` + if [ ! "$containers" = "" ]; then + sudo docker rm -f $containers || true + fi } # configure_zun() - Set config files, create data dirs, etc @@ -108,15 +137,11 @@ function configure_zun { create_zun_conf create_api_paste_conf - - if [[ ${ZUN_DRIVER} == "docker" ]]; then - check_docker || install_docker - fi } # upload_sandbox_image() - Upload sandbox image to glance function upload_sandbox_image { - if [[ ${ZUN_DRIVER} == "docker" ]]; then + if [[ ${ZUN_DRIVER} == "docker" || ${ZUN_DRIVER} == "nova-docker" ]]; then sg docker "docker pull kubernetes/pause" sg docker "docker save kubernetes/pause" | openstack image create kubernetes/pause --public --container-format docker --disk-format raw fi @@ -149,6 +174,11 @@ function create_zun_conf { # (Re)create ``zun.conf`` rm -f $ZUN_CONF + if [[ ${ZUN_DRIVER} == "docker" ]]; then + iniset $ZUN_CONF DEFAULT container_driver docker.driver.DockerDriver + elif [[ ${ZUN_DRIVER} == "nova-docker" ]]; then + iniset $ZUN_CONF DEFAULT container_driver docker.driver.NovaDockerDriver + fi iniset $ZUN_CONF DEFAULT debug "$ENABLE_DEBUG_LOG_LEVEL" iniset $ZUN_CONF oslo_messaging_rabbit rabbit_userid $RABBIT_USERID iniset $ZUN_CONF oslo_messaging_rabbit rabbit_password $RABBIT_PASSWORD @@ -182,6 +212,8 @@ function create_zun_conf { ${KEYSTONE_SERVICE_PROTOCOL}://${HOST_IP}:${KEYSTONE_SERVICE_PORT}/v3 iniset $ZUN_CONF keystone_authtoken auth_version v3 + iniset $ZUN_CONF glance images_directory $ZUN_STATE_PATH/images + if is_fedora || is_suse; then # zun defaults to /usr/local/bin, but fedora and suse pip like to # install things in /usr/bin @@ -263,26 +295,11 @@ function install_zun { setup_develop $ZUN_DIR } -function install_docker { - echo "Installing docker" - curl -fsSL https://get.docker.com/ | sudo sh - echo "Adding $(whoami) to ${DOCKER_GROUP}..." - sudo usermod -a -G ${DOCKER_GROUP} $(whoami) - newgrp ${DOCKER_GROUP} - if is_ubuntu && [ $UBUNTU_RELEASE_BASE_NUM -le 14 ]; then - sudo service docker start || true - else - sudo systemctl enable docker.service - sudo systemctl start docker || true - fi -} - function install_etcd_server { echo "Installing etcd" - check_docker || install_docker # If there's a container named 'etcd' already exists, remove it. if [ $(sudo docker ps -a | awk '{print $NF}' | grep -w etcd) ]; then - sudo docker rm -f etcd + sudo docker rm -f etcd || true fi sudo docker run -d --net=host --name etcd quay.io/coreos/etcd:${ETCD_VERSION} \ /usr/local/bin/etcd \ @@ -322,7 +339,7 @@ function start_zun_api { # start_zun_compute() - Start Zun compute agent function start_zun_compute { echo "Start zun compute..." - if [[ ${ZUN_DRIVER} == "docker" ]]; then + if [[ ${ZUN_DRIVER} == "docker" || ${ZUN_DRIVER} == "nova-docker" ]]; then run_process zun-compute "$ZUN_BIN_DIR/zun-compute" ${DOCKER_GROUP} else run_process zun-compute "$ZUN_BIN_DIR/zun-compute" @@ -337,6 +354,7 @@ function start_zun_etcd { function stop_zun-etcd { echo "Stop zun etcd..." sudo docker stop etcd + sudo docker rm -f etcd || true } # start_zun() - Start running processes, including screen diff --git a/devstack/override-defaults b/devstack/override-defaults new file mode 100644 index 000000000..bd47dcde4 --- /dev/null +++ b/devstack/override-defaults @@ -0,0 +1,7 @@ +# Plug-in overrides + +ZUN_DRIVER=${ZUN_DRIVER:-docker} + +if [[ ${ZUN_DRIVER} == "nova-docker" ]]; then + export VIRT_DRIVER=docker +fi diff --git a/devstack/plugin.sh b/devstack/plugin.sh index acae9d6bd..2a0fc9eff 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -6,11 +6,13 @@ set -o xtrace echo_summary "zun's plugin.sh was called..." source $DEST/zun/devstack/lib/zun +source $DEST/zun/devstack/lib/nova (set -o posix; set) if is_service_enabled zun-api zun-compute; then if [[ "$1" == "stack" && "$2" == "install" ]]; then echo_summary "Installing zun" + install_docker install_zun LIBS_FROM_GIT="${LIBS_FROM_GIT},python-zunclient" @@ -25,6 +27,10 @@ if is_service_enabled zun-api zun-compute; then create_zun_accounts fi + if [[ ${ZUN_DRIVER} == "nova-docker" ]]; then + configure_nova_docker + fi + elif [[ "$1" == "stack" && "$2" == "extra" ]]; then # Initialize zun init_zun diff --git a/etc/nova/rootwrap.d/docker.filters b/etc/nova/rootwrap.d/docker.filters new file mode 100644 index 000000000..dc75f6fd8 --- /dev/null +++ b/etc/nova/rootwrap.d/docker.filters @@ -0,0 +1,6 @@ +# nova-rootwrap command filters for setting up network in the docker driver +# This file should be owned by (and only-writeable by) the root user + +[Filters] +# nova/virt/docker/driver.py: 'ln', '-sf', '/var/run/netns/.*' +ln: CommandFilter, /bin/ln, root diff --git a/nova/__init__.py b/nova/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/virt/__init__.py b/nova/virt/__init__.py new file mode 100644 index 000000000..23f56962b --- /dev/null +++ b/nova/virt/__init__.py @@ -0,0 +1,16 @@ +# All Rights Reserved. +# +# 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. + +# Allow composition of nova.virt namespace from disparate packages. +__import__('pkg_resources').declare_namespace(__name__) diff --git a/nova/virt/docker/__init__.py b/nova/virt/docker/__init__.py new file mode 100644 index 000000000..93544a795 --- /dev/null +++ b/nova/virt/docker/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2013 dotCloud, Inc. +# All Rights Reserved. +# +# 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. + +""" +:mod:`docker` -- Nova support for Docker Hypervisor to run Linux containers +=========================================================================== +""" +from nova.virt.docker import driver + +DockerDriver = driver.DockerDriver diff --git a/nova/virt/docker/client.py b/nova/virt/docker/client.py new file mode 100644 index 000000000..f081eeff2 --- /dev/null +++ b/nova/virt/docker/client.py @@ -0,0 +1,99 @@ +# Copyright (c) 2013 dotCloud, Inc. +# Copyright 2014 IBM Corp. +# All Rights Reserved. +# +# 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. + +import functools +import inspect + +from oslo_config import cfg +import six + +from docker import client +from docker import tls + +CONF = cfg.CONF +DEFAULT_TIMEOUT_SECONDS = 120 +DEFAULT_DOCKER_API_VERSION = '1.19' + + +def filter_data(f): + """Decorator that post-processes data returned by Docker. + + This will avoid any surprises with different versions of Docker. + """ + @functools.wraps(f, assigned=[]) + def wrapper(*args, **kwds): + out = f(*args, **kwds) + + def _filter(obj): + if isinstance(obj, list): + new_list = [] + for o in obj: + new_list.append(_filter(o)) + obj = new_list + if isinstance(obj, dict): + for k, v in obj.items(): + if isinstance(k, six.string_types): + obj[k.lower()] = _filter(v) + return obj + return _filter(out) + return wrapper + + +class DockerHTTPClient(client.Client): + def __init__(self, url='unix://var/run/docker.sock'): + if (CONF.docker.cert_file or + CONF.docker.key_file): + client_cert = (CONF.docker.cert_file, CONF.docker.key_file) + else: + client_cert = None + if (CONF.docker.ca_file or + CONF.docker.api_insecure or + client_cert): + ssl_config = tls.TLSConfig( + client_cert=client_cert, + ca_cert=CONF.docker.ca_file, + verify=CONF.docker.api_insecure) + else: + ssl_config = False + super(DockerHTTPClient, self).__init__( + base_url=url, + version=DEFAULT_DOCKER_API_VERSION, + timeout=DEFAULT_TIMEOUT_SECONDS, + tls=ssl_config + ) + self._setup_decorators() + + def _setup_decorators(self): + for name, member in inspect.getmembers(self, inspect.ismethod): + if not name.startswith('_'): + setattr(self, name, filter_data(member)) + + def pause(self, container_id): + url = self._url("/containers/{0}/pause".format(container_id)) + res = self._post(url) + return res.status_code == 204 + + def unpause(self, container_id): + url = self._url("/containers/{0}/unpause".format(container_id)) + res = self._post(url) + return res.status_code == 204 + + def load_repository_file(self, name, path): + with open(path, 'rb') as fh: + self.load_image(fh) + + def get_container_logs(self, container_id): + return self.attach(container_id, 1, 1, 0, 1) diff --git a/nova/virt/docker/driver.py b/nova/virt/docker/driver.py new file mode 100644 index 000000000..e9c3dbb37 --- /dev/null +++ b/nova/virt/docker/driver.py @@ -0,0 +1,754 @@ +# Copyright (c) 2013 dotCloud, Inc. +# Copyright 2014 IBM Corp. +# All Rights Reserved. +# +# 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. + +""" +A Docker Hypervisor which allows running Linux Containers instead of VMs. +""" + +import os +import shutil +import socket +import time +import uuid + +from docker import errors +import eventlet +from oslo_config import cfg +from oslo_log import log +from oslo_utils import fileutils +from oslo_utils import importutils +from oslo_utils import units +from oslo_utils import versionutils + +from nova.compute import arch +from nova.compute import flavors +from nova.compute import hv_type +from nova.compute import power_state +from nova.compute import vm_mode +from nova import exception +from nova.i18n import _ +from nova.i18n import _LE +from nova.i18n import _LI +from nova.i18n import _LW +from nova import objects +from nova import utils +from nova.virt.docker import client as docker_client +from nova.virt.docker import hostinfo +from nova.virt.docker import network +from nova.virt import driver +from nova.virt import firewall +from nova.virt import hardware +from nova.virt import hostutils +from nova.virt import images + +CONF = cfg.CONF +CONF.import_opt('my_ip', 'nova.conf.netconf') +CONF.import_opt('instances_path', 'nova.compute.manager') + +docker_opts = [ + cfg.StrOpt('root_directory', + default='/var/lib/docker', + help='Path to use as the root of the Docker runtime.'), + cfg.StrOpt('host_url', + default='unix:///var/run/docker.sock', + help='tcp://host:port to bind/connect to or ' + 'unix://path/to/socket to use'), + cfg.BoolOpt('api_insecure', + default=False, + help='If set, ignore any SSL validation issues'), + cfg.StrOpt('ca_file', + help='Location of CA certificates file for ' + 'securing docker api requests (tlscacert).'), + cfg.StrOpt('cert_file', + help='Location of TLS certificate file for ' + 'securing docker api requests (tlscert).'), + cfg.StrOpt('key_file', + help='Location of TLS private key file for ' + 'securing docker api requests (tlskey).'), + cfg.StrOpt('vif_driver', + default='nova.virt.docker.vifs.DockerGenericVIFDriver'), + cfg.StrOpt('snapshots_directory', + default='$instances_path/snapshots', + help='Location where docker driver will temporarily store ' + 'snapshots.'), + cfg.StrOpt('shared_directory', + default=None, + help='Shared directory where glance images located. If ' + 'specified, docker will try to load the image from ' + 'the shared directory by image ID.'), + cfg.BoolOpt('privileged', + default=False, + help='Set true can own all root privileges in a container.'), + cfg.ListOpt('default_nameservers', + default=['8.8.8.8', '8.8.4.4'], + help='The default DNS server to use.'), +] + +CONF.register_opts(docker_opts, 'docker') + +LOG = log.getLogger(__name__) + + +class DockerDriver(driver.ComputeDriver): + """Docker hypervisor driver.""" + + def __init__(self, virtapi): + super(DockerDriver, self).__init__(virtapi) + self._docker = None + vif_class = importutils.import_class(CONF.docker.vif_driver) + self.vif_driver = vif_class() + self.firewall_driver = firewall.load_driver( + default='nova.virt.firewall.NoopFirewallDriver') + # NOTE(zhangguoqing): For passing the nova unit tests + self.active_migrations = {} + + @property + def docker(self): + if self._docker is None: + self._docker = docker_client.DockerHTTPClient(CONF.docker.host_url) + return self._docker + + def init_host(self, host): + if self._is_daemon_running() is False: + raise exception.NovaException( + _('Docker daemon is not running or is not reachable' + ' (check the rights on /var/run/docker.sock)')) + + def _is_daemon_running(self): + return self.docker.ping() + + def _start_firewall(self, instance, network_info): + self.firewall_driver.setup_basic_filtering(instance, network_info) + self.firewall_driver.prepare_instance_filter(instance, network_info) + self.firewall_driver.apply_instance_filter(instance, network_info) + + def _stop_firewall(self, instance, network_info): + self.firewall_driver.unfilter_instance(instance, network_info) + + def refresh_security_group_rules(self, security_group_id): + """Refresh security group rules from data store. + + Invoked when security group rules are updated. + + :param security_group_id: The security group id. + + """ + self.firewall_driver.refresh_security_group_rules(security_group_id) + + def refresh_security_group_members(self, security_group_id): + """Refresh security group members from data store. + + Invoked when instances are added/removed to a security group. + + :param security_group_id: The security group id. + + """ + self.firewall_driver.refresh_security_group_members(security_group_id) + + def refresh_provider_fw_rules(self): + """Triggers a firewall update based on database changes.""" + self.firewall_driver.refresh_provider_fw_rules() + + def refresh_instance_security_rules(self, instance): + """Refresh security group rules from data store. + + Gets called when an instance gets added to or removed from + the security group the instance is a member of or if the + group gains or loses a rule. + + :param instance: The instance object. + + """ + self.firewall_driver.refresh_instance_security_rules(instance) + + def ensure_filtering_rules_for_instance(self, instance, network_info): + """Set up filtering rules. + + :param instance: The instance object. + :param network_info: Instance network information. + + """ + self.firewall_driver.setup_basic_filtering(instance, network_info) + self.firewall_driver.prepare_instance_filter(instance, network_info) + + def unfilter_instance(self, instance, network_info): + """Stop filtering instance. + + :param instance: The instance object. + :param network_info: Instance network information. + + """ + self.firewall_driver.unfilter_instance(instance, network_info) + + def list_instances(self, inspect=False): + res = [] + for container in self.docker.containers(all=True): + info = self.docker.inspect_container(container['id']) + if not info: + continue + if inspect: + res.append(info) + else: + res.append(info['Config'].get('Hostname')) + return res + + def attach_interface(self, instance, image_meta, vif): + """Attach an interface to the container.""" + self.vif_driver.plug(instance, vif) + container_id = self._find_container_by_instance(instance).get('id') + self.vif_driver.attach(instance, vif, container_id) + + def detach_interface(self, instance, vif): + """Detach an interface from the container.""" + self.vif_driver.unplug(instance, vif) + + def plug_vifs(self, instance, network_info): + """Plug VIFs into networks.""" + for vif in network_info: + self.vif_driver.plug(instance, vif) + self._start_firewall(instance, network_info) + + def _attach_vifs(self, instance, network_info): + """Plug VIFs into container.""" + if not network_info: + return + + if os.name == 'nt': + return + + container_id = self._get_container_id(instance) + if not container_id: + raise exception.InstanceNotFound(instance_id=instance['name']) + netns_path = '/var/run/netns' + if not os.path.exists(netns_path): + utils.execute( + 'mkdir', '-p', netns_path, run_as_root=True) + nspid = self._find_container_pid(container_id) + if not nspid: + msg = _('Cannot find any PID under container "{0}"') + raise RuntimeError(msg.format(container_id)) + netns_path = os.path.join(netns_path, container_id) + utils.execute( + 'ln', '-sf', '/proc/{0}/ns/net'.format(nspid), + '/var/run/netns/{0}'.format(container_id), + run_as_root=True) + utils.execute('ip', 'netns', 'exec', container_id, 'ip', 'link', + 'set', 'lo', 'up', run_as_root=True) + + for vif in network_info: + self.vif_driver.attach(instance, vif, container_id) + + def unplug_vifs(self, instance, network_info): + """Unplug VIFs from networks.""" + for vif in network_info: + self.vif_driver.unplug(instance, vif) + self._stop_firewall(instance, network_info) + + def _encode_utf8(self, value): + return unicode(value).encode('utf-8') + + def _find_container_by_instance(self, instance): + try: + name = self._get_container_name(instance) + containers = self.docker.containers(all=True, + filters={'name': name}) + if containers: + # NOTE(dims): We expect only one item in the containers list + return self.docker.inspect_container(containers[0]['id']) + except errors.APIError as e: + if e.response.status_code != 404: + raise + return {} + + def _get_container_name(self, instance): + return "zun-sandbox-" + instance['uuid'] + + def _get_container_id(self, instance): + return self._find_container_by_instance(instance).get('id') + + def get_info(self, instance): + container = self._find_container_by_instance(instance) + if not container: + raise exception.InstanceNotFound(instance_id=instance['name']) + running = container['State'].get('Running') + mem = container['Config'].get('Memory', 0) + + # NOTE(ewindisch): cgroups/lxc defaults to 1024 multiplier. + # see: _get_cpu_shares for further explaination + num_cpu = container['Config'].get('CpuShares', 0) / 1024 + + # FIXME(ewindisch): Improve use of statistics: + # For 'mem', we should expose memory.stat.rss, and + # for cpu_time we should expose cpuacct.stat.system, + # but these aren't yet exposed by Docker. + # + # Also see: + # docker/docs/sources/articles/runmetrics.md + info = hardware.InstanceInfo( + max_mem_kb=mem, + mem_kb=mem, + num_cpu=num_cpu, + cpu_time_ns=0, + state=(power_state.RUNNING if running + else power_state.SHUTDOWN) + ) + return info + + def get_host_stats(self, refresh=False): + hostname = socket.gethostname() + stats = self.get_available_resource(hostname) + stats['host_hostname'] = stats['hypervisor_hostname'] + stats['host_name_label'] = stats['hypervisor_hostname'] + return stats + + def get_available_nodes(self, refresh=False): + hostname = socket.gethostname() + return [hostname] + + def get_available_resource(self, nodename): + if not hasattr(self, '_nodename'): + self._nodename = nodename + if nodename != self._nodename: + LOG.error(_('Hostname has changed from %(old)s to %(new)s. ' + 'A restart is required to take effect.' + ), {'old': self._nodename, + 'new': nodename}) + + memory = hostinfo.get_memory_usage() + disk = hostinfo.get_disk_usage() + stats = { + 'vcpus': hostinfo.get_total_vcpus(), + 'vcpus_used': hostinfo.get_vcpus_used(self.list_instances(True)), + 'memory_mb': memory['total'] / units.Mi, + 'memory_mb_used': memory['used'] / units.Mi, + 'local_gb': disk['total'] / units.Gi, + 'local_gb_used': disk['used'] / units.Gi, + 'disk_available_least': disk['available'] / units.Gi, + 'hypervisor_type': 'docker', + 'hypervisor_version': versionutils.convert_version_to_int('1.0'), + 'hypervisor_hostname': self._nodename, + 'cpu_info': '?', + 'numa_topology': None, + 'supported_instances': [ + (arch.I686, hv_type.DOCKER, vm_mode.EXE), + (arch.X86_64, hv_type.DOCKER, vm_mode.EXE) + ] + } + return stats + + def _find_container_pid(self, container_id): + n = 0 + while True: + # NOTE(samalba): We wait for the process to be spawned inside the + # container in order to get the the "container pid". This is + # usually really fast. To avoid race conditions on a slow + # machine, we allow 10 seconds as a hard limit. + if n > 20: + return + info = self.docker.inspect_container(container_id) + if info: + pid = info['State']['Pid'] + # Pid is equal to zero if it isn't assigned yet + if pid: + return pid + time.sleep(0.5) + n += 1 + + def _get_memory_limit_bytes(self, instance): + if isinstance(instance, objects.Instance): + return instance.get_flavor().memory_mb * units.Mi + else: + system_meta = utils.instance_sys_meta(instance) + return int(system_meta.get( + 'instance_type_memory_mb', 0)) * units.Mi + + def _get_image_name(self, context, instance, image): + fmt = image.container_format + if fmt != 'docker': + msg = _('Image container format not supported ({0})') + raise exception.InstanceDeployFailure(msg.format(fmt), + instance_id=instance['name']) + return image.name + + def _pull_missing_image(self, context, image_meta, instance): + msg = 'Image name "%s" does not exist, fetching it...' + LOG.debug(msg, image_meta.name) + + shared_directory = CONF.docker.shared_directory + if (shared_directory and + os.path.exists(os.path.join(shared_directory, + image_meta.id))): + LOG.debug('Found %s in shared_directory', image_meta.id) + try: + LOG.debug('Loading repository file into docker %s', + self._encode_utf8(image_meta.name)) + self.docker.load_repository_file( + self._encode_utf8(image_meta.name), + os.path.join(shared_directory, image_meta.id)) + return self.docker.inspect_image( + self._encode_utf8(image_meta.name)) + except Exception as e: + # If failed to load image from shared_directory, continue + # to download the image from glance then load. + LOG.warning(_('Cannot load repository file from shared ' + 'directory: %s'), + e, instance=instance, exc_info=True) + + # TODO(imain): It would be nice to do this with file like object + # passing but that seems a bit complex right now. + snapshot_directory = CONF.docker.snapshots_directory + fileutils.ensure_tree(snapshot_directory) + with utils.tempdir(dir=snapshot_directory) as tmpdir: + try: + out_path = os.path.join(tmpdir, uuid.uuid4().hex) + + LOG.debug('Fetching image with id %s from glance', + image_meta.id) + images.fetch(context, image_meta.id, out_path) + LOG.debug('Loading repository file into docker %s', + self._encode_utf8(image_meta.name)) + self.docker.load_repository_file( + self._encode_utf8(image_meta.name), + out_path + ) + return self.docker.inspect_image( + self._encode_utf8(image_meta.name)) + except Exception as e: + LOG.warning(_('Cannot load repository file: %s'), + e, instance=instance, exc_info=True) + msg = _('Cannot load repository file: {0}') + raise exception.NovaException(msg.format(e), + instance_id=image_meta.name) + + def _create_instance_file(self, id, name, data): + file_dir = os.path.join(CONF.instances_path, id) + fileutils.ensure_tree(file_dir) + file = os.path.join(file_dir, name) + with open(file, 'a') as f: + f.write(data) + os.chmod(file_dir, 0o700) + os.chmod(file, 0o600) + return file + + def _cleanup_instance_file(self, id): + dir = os.path.join(CONF.instances_path, id) + if os.path.exists(dir): + LOG.info(_LI('Deleting instance files %s'), dir) + try: + shutil.rmtree(dir) + except OSError as e: + LOG.error(_LE('Failed to cleanup directory %(target)s: ' + '%(e)s'), {'target': dir, 'e': e}) + + def _neutron_failed_callback(self, event_name, instance): + LOG.error(_LE('Neutron Reported failure on event ' + '%(event)s for instance %(uuid)s'), + {'event': event_name, 'uuid': instance.uuid}, + instance=instance) + if CONF.vif_plugging_is_fatal: + raise exception.VirtualInterfaceCreateException() + + def _get_neutron_events(self, network_info): + # NOTE(danms): We need to collect any VIFs that are currently + # down that we expect a down->up event for. Anything that is + # already up will not undergo that transition, and for + # anything that might be stale (cache-wise) assume it's + # already up so we don't block on it. + return [('network-vif-plugged', vif['id']) + for vif in network_info if vif.get('active', True) is False] + + def _start_container(self, container_id, instance, network_info=None): + self.docker.start(container_id) + + if not network_info: + return + timeout = CONF.vif_plugging_timeout + if (utils.is_neutron() and timeout): + events = self._get_neutron_events(network_info) + else: + events = [] + + try: + with self.virtapi.wait_for_instance_event( + instance, events, deadline=timeout, + error_callback=self._neutron_failed_callback): + self.plug_vifs(instance, network_info) + self._attach_vifs(instance, network_info) + except eventlet.timeout.Timeout: + LOG.warn(_LW('Timeout waiting for vif plugging callback for ' + 'instance %(uuid)s'), {'uuid': instance['name']}) + if CONF.vif_plugging_is_fatal: + self.docker.kill(container_id) + self.docker.remove_container(container_id, force=True) + raise exception.InstanceDeployFailure( + 'Timeout waiting for vif plugging', + instance_id=instance['name']) + except (Exception) as e: + LOG.warning(_('Cannot setup network: %s'), + e, instance=instance, exc_info=True) + msg = _('Cannot setup network: {0}') + self.docker.kill(container_id) + self.docker.remove_container(container_id, force=True) + raise exception.InstanceDeployFailure(msg.format(e), + instance_id=instance['name']) + + def spawn(self, context, instance, image_meta, injected_files, + admin_password, network_info=None, block_device_info=None, + flavor=None): + image_name = self._get_image_name(context, instance, image_meta) + args = { + 'hostname': instance['display_name'], + 'mem_limit': self._get_memory_limit_bytes(instance), + 'cpu_shares': self._get_cpu_shares(instance), + 'network_disabled': True, + 'privileged': CONF.docker.privileged, + 'binds': self._get_binds(instance, network_info), + } + + try: + image = self.docker.inspect_image(self._encode_utf8(image_name)) + except errors.APIError: + image = None + + if not image: + image = self._pull_missing_image(context, image_meta, instance) + + container = self._create_container(instance, image_name, args) + if not container: + raise exception.InstanceDeployFailure( + _('Cannot create container'), + instance_id=instance['name']) + + container_id = container['Id'] + self._start_container(container_id, instance, network_info) + + def _get_binds(self, instance, network_info): + binds = [] + dns = self._extract_dns_entries(network_info) + bind = self._get_resolvconf_bind(instance['uuid'], dns) + binds.append(bind) + + hostname = instance['display_name'] + bind = self._get_hostname_bind(instance['uuid'], hostname) + binds.append(bind) + + bind = self._get_hosts_bind(instance['uuid'], hostname) + binds.append(bind) + return binds + + def _extract_dns_entries(self, network_info): + dns = [] + if network_info: + for net in network_info: + subnets = net['network'].get('subnets', []) + for subnet in subnets: + dns_entries = subnet.get('dns', []) + for dns_entry in dns_entries: + if 'address' in dns_entry: + dns.append(dns_entry['address']) + return dns if dns else CONF.docker.default_nameservers + + def _get_resolvconf_bind(self, instance_id, nameservers): + data = '\n'.join('nameserver %(server)s' % {'server': s} + for s in nameservers) + file_name = 'resolv.conf' + host_src = self._create_instance_file(instance_id, file_name, data) + bind = '%(host_src)s:/etc/resolv.conf:ro' % {'host_src': host_src} + return bind + + def _get_hostname_bind(self, instance_id, hostname): + data = hostname + file_name = 'hostname' + host_src = self._create_instance_file(instance_id, file_name, data) + bind = '%(host_src)s:/etc/hostname:ro' % {'host_src': host_src} + return bind + + def _get_hosts_bind(self, instance_id, hostname): + data = ('127.0.0.1 localhost ' + hostname + '\n' + '::1 localhost ip6-localhost ip6-loopback\n' + 'fe00::0 ip6-localnet\n' + 'ff00::0 ip6-mcastprefix\n' + 'ff02::1 ip6-allnodes\n' + 'ff02::2 ip6-allrouters\n') + file_name = 'hosts' + host_src = self._create_instance_file(instance_id, file_name, data) + bind = '%(host_src)s:/etc/hosts:ro' % {'host_src': host_src} + return bind + + def restore(self, instance): + container_id = self._get_container_id(instance) + if not container_id: + return + + self._start_container(container_id, instance) + + def _stop(self, container_id, instance, timeout=5): + try: + self.docker.stop(container_id, max(timeout, 5)) + except errors.APIError as e: + if 'Unpause the container before stopping' not in e.explanation: + LOG.warning(_('Cannot stop container: %s'), + e, instance=instance, exc_info=True) + raise + self.docker.unpause(container_id) + self.docker.stop(container_id, timeout) + + def soft_delete(self, instance): + container_id = self._get_container_id(instance) + if not container_id: + return + self._stop(container_id, instance) + + def destroy(self, context, instance, network_info, block_device_info=None, + destroy_disks=True, migrate_data=None): + self.soft_delete(instance) + container_id = self._get_container_id(instance) + if container_id: + self.docker.remove_container(container_id, force=True) + self.cleanup(context, instance, network_info, + block_device_info, destroy_disks) + + def cleanup(self, context, instance, network_info, block_device_info=None, + destroy_disks=True, migrate_data=None, destroy_vifs=True): + """Cleanup after instance being destroyed by Hypervisor.""" + container_id = self._get_container_id(instance) + if not container_id: + self.unplug_vifs(instance, network_info) + return + network.teardown_network(container_id) + self.unplug_vifs(instance, network_info) + self._cleanup_instance_file(instance['uuid']) + + def reboot(self, context, instance, network_info, reboot_type, + block_device_info=None, bad_volumes_callback=None): + container_id = self._get_container_id(instance) + if not container_id: + return + self._stop(container_id, instance) + try: + network.teardown_network(container_id) + if network_info: + self.unplug_vifs(instance, network_info) + except Exception as e: + LOG.warning(_('Cannot destroy the container network' + ' during reboot {0}').format(e), + exc_info=True) + return + + self.docker.start(container_id) + try: + if network_info: + self.plug_vifs(instance, network_info) + self._attach_vifs(instance, network_info) + except Exception as e: + LOG.warning(_('Cannot setup network on reboot: {0}'), e, + exc_info=True) + return + + def power_on(self, context, instance, network_info, + block_device_info=None): + container_id = self._get_container_id(instance) + if not container_id: + return + self.docker.start(container_id) + if not network_info: + return + try: + self.plug_vifs(instance, network_info) + self._attach_vifs(instance, network_info) + except Exception as e: + LOG.debug(_('Cannot setup network: %s'), + e, instance=instance, exc_info=True) + msg = _('Cannot setup network: {0}') + self.docker.kill(container_id) + self.docker.remove_container(container_id, force=True) + raise exception.InstanceDeployFailure(msg.format(e), + instance_id=instance['name']) + + def power_off(self, instance, timeout=0, retry_interval=0): + container_id = self._get_container_id(instance) + if not container_id: + return + self._stop(container_id, instance, timeout) + + def pause(self, instance): + """Pause the specified instance. + + :param instance: nova.objects.instance.Instance + """ + try: + cont_id = self._get_container_id(instance) + if not self.docker.pause(cont_id): + raise exception.NovaException + except Exception as e: + LOG.debug(_('Error pause container: %s'), + e, instance=instance, exc_info=True) + msg = _('Cannot pause container: {0}') + raise exception.NovaException(msg.format(e), + instance_id=instance['name']) + + def unpause(self, instance): + """Unpause paused VM instance. + + :param instance: nova.objects.instance.Instance + """ + try: + cont_id = self._get_container_id(instance) + if not self.docker.unpause(cont_id): + raise exception.NovaException + except Exception as e: + LOG.debug(_('Error unpause container: %s'), + e, instance=instance, exc_info=True) + msg = _('Cannot unpause container: {0}') + raise exception.NovaException(msg.format(e), + instance_id=instance['name']) + + def _get_cpu_shares(self, instance): + """Get allocated CPUs from configured flavor. + + Docker/lxc supports relative CPU allocation. + + cgroups specifies following: + /sys/fs/cgroup/lxc/cpu.shares = 1024 + /sys/fs/cgroup/cpu.shares = 1024 + + For that reason we use 1024 as multiplier. + This multiplier allows to divide the CPU + resources fair with containers started by + the user (e.g. docker registry) which has + the default CpuShares value of zero. + """ + if isinstance(instance, objects.Instance): + flavor = instance.get_flavor() + else: + flavor = flavors.extract_flavor(instance) + return int(flavor['vcpus']) * 1024 + + def _create_container(self, instance, image_name, args): + name = self._get_container_name(instance) + hostname = args.pop('hostname', None) + cpu_shares = args.pop('cpu_shares', None) + network_disabled = args.pop('network_disabled', False) + host_config = self.docker.create_host_config(**args) + return self.docker.create_container(image_name, + name=self._encode_utf8(name), + hostname=hostname, + cpu_shares=cpu_shares, + network_disabled=network_disabled, + host_config=host_config) + + def get_host_uptime(self): + return hostutils.sys_uptime() diff --git a/nova/virt/docker/hostinfo.py b/nova/virt/docker/hostinfo.py new file mode 100644 index 000000000..f4243596f --- /dev/null +++ b/nova/virt/docker/hostinfo.py @@ -0,0 +1,74 @@ +# Copyright (c) 2013 dotCloud, Inc. +# All Rights Reserved. +# +# 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. + +import multiprocessing +import os + +from oslo_config import cfg +import psutil + +CONF = cfg.CONF + + +def statvfs(): + docker_path = CONF.docker.root_directory + if not os.path.exists(docker_path): + docker_path = '/' + return psutil.disk_usage(docker_path) + + +def get_disk_usage(): + # This is the location where Docker stores its containers. It's currently + # hardcoded in Docker so it's not configurable yet. + st = statvfs() + return { + 'total': st.total, + 'available': st.free, + 'used': st.used + } + + +def get_total_vcpus(): + return multiprocessing.cpu_count() + + +def get_vcpus_used(containers): + total_vcpus_used = 0 + for container in containers: + if isinstance(container, dict): + total_vcpus_used += container.get('Config', {}).get( + 'CpuShares', 0) / 1024 + + return total_vcpus_used + + +def get_memory_usage(): + vmem = psutil.virtual_memory() + return { + 'total': vmem.total, + 'used': vmem.used + } + + +def get_mounts(): + with open('/proc/mounts') as f: + return f.readlines() + + +def get_cgroup_devices_path(): + for ln in get_mounts(): + fields = ln.split(' ') + if fields[2] == 'cgroup' and 'devices' in fields[3].split(','): + return fields[1] diff --git a/nova/virt/docker/network.py b/nova/virt/docker/network.py new file mode 100644 index 000000000..8e040b3dd --- /dev/null +++ b/nova/virt/docker/network.py @@ -0,0 +1,71 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +import os + +from oslo_concurrency import processutils +from oslo_log import log + +from nova import exception +from nova import utils + +from nova.i18n import _ + +LOG = log.getLogger(__name__) + + +def teardown_network(container_id): + if os.name == 'nt': + return + + try: + output, err = utils.execute('ip', '-o', 'netns', 'list') + for line in output.split('\n'): + if container_id == line.strip(): + utils.execute('ip', 'netns', 'delete', container_id, + run_as_root=True) + break + except processutils.ProcessExecutionError: + LOG.warning(_('Cannot remove network namespace, netns id: %s'), + container_id) + + +def find_fixed_ips(instance, network_info): + ips = [] + for subnet in network_info['subnets']: + netmask = subnet['cidr'].split('/')[1] + for ip in subnet['ips']: + if ip['type'] == 'fixed' and ip['address']: + ips.append(ip['address'] + "/" + netmask) + if not ips: + raise exception.InstanceDeployFailure(_('Cannot find fixed ip'), + instance_id=instance['uuid']) + return ips + + +def find_gateways(instance, network_info): + gateways = [] + for subnet in network_info['subnets']: + gateways.append(subnet['gateway']['address']) + if not gateways: + raise exception.InstanceDeployFailure(_('Cannot find gateway'), + instance_id=instance['uuid']) + return gateways + + +# NOTE(arosen) - this method should be removed after it's moved into the +# linux_net code in nova. +def get_ovs_interfaceid(vif): + return vif.get('ovs_interfaceid') or vif['id'] diff --git a/nova/virt/docker/opencontrail.py b/nova/virt/docker/opencontrail.py new file mode 100644 index 000000000..4dbcdab0c --- /dev/null +++ b/nova/virt/docker/opencontrail.py @@ -0,0 +1,177 @@ +# Copyright (C) 2014 Juniper Networks, Inc +# All Rights Reserved. +# +# 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. + +import eventlet + +from contrail_vrouter_api.vrouter_api import ContrailVRouterApi + +from nova.network import linux_net +from nova import utils + +from oslo_log import log as logging +from oslo_service import loopingcall + +from nova.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class OpenContrailVIFDriver(object): + def __init__(self): + self._vrouter_semaphore = eventlet.semaphore.Semaphore() + self._vrouter_client = ContrailVRouterApi( + doconnect=True, semaphore=self._vrouter_semaphore) + timer = loopingcall.FixedIntervalLoopingCall(self._keep_alive) + timer.start(interval=2) + + def _keep_alive(self): + self._vrouter_client.periodic_connection_check() + + def plug(self, instance, vif): + vif_type = vif['type'] + + LOG.debug('Plug vif_type=%(vif_type)s instance=%(instance)s ' + 'vif=%(vif)s', + {'vif_type': vif_type, 'instance': instance, + 'vif': vif}) + + if_local_name = 'veth%s' % vif['id'][:8] + if_remote_name = 'ns%s' % vif['id'][:8] + + # Device already exists so return. + if linux_net.device_exists(if_local_name): + return + undo_mgr = utils.UndoManager() + + try: + utils.execute('ip', 'link', 'add', if_local_name, 'type', 'veth', + 'peer', 'name', if_remote_name, run_as_root=True) + undo_mgr.undo_with(lambda: utils.execute( + 'ip', 'link', 'delete', if_local_name, run_as_root=True)) + + utils.execute('ip', 'link', 'set', if_remote_name, 'address', + vif['address'], run_as_root=True) + + except Exception: + LOG.exception("Failed to configure network") + msg = _('Failed to setup the network, rolling back') + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + def attach(self, instance, vif, container_id): + vif_type = vif['type'] + + LOG.debug('Attach vif_type=%(vif_type)s instance=%(instance)s ' + 'vif=%(vif)s', + {'vif_type': vif_type, 'instance': instance, + 'vif': vif}) + + if_local_name = 'veth%s' % vif['id'][:8] + if_remote_name = 'ns%s' % vif['id'][:8] + + undo_mgr = utils.UndoManager() + undo_mgr.undo_with(lambda: utils.execute( + 'ip', 'link', 'delete', if_local_name, run_as_root=True)) + ipv4_address, ipv4_netmask, ipv4_gateway = self._retrieve_ip_address( + vif, 4) + ipv6_address, ipv6_netmask, ipv6_gateway = self._retrieve_ip_address( + vif, 6) + ipv4_address = ipv4_address or '0.0.0.0' + params = { + 'ip_address': ipv4_address, + 'vn_id': vif['network']['id'], + 'display_name': instance['display_name'], + 'hostname': instance['hostname'], + 'host': instance['host'], + 'vm_project_id': instance['project_id'], + 'port_type': 'NovaVMPort', + 'ip6_address': ipv6_address, + } + + try: + utils.execute('ip', 'link', 'set', if_remote_name, 'netns', + container_id, run_as_root=True) + + result = self._vrouter_client.add_port( + instance['uuid'], vif['id'], + if_local_name, vif['address'], **params) + if not result: + # follow the exception path + raise RuntimeError('add_port returned %s' % str(result)) + utils.execute('ip', 'link', 'set', if_local_name, 'up', + run_as_root=True) + except Exception: + LOG.exception("Failed to attach the network") + msg = _('Failed to attach the network, rolling back') + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + try: + utils.execute('ip', 'netns', 'exec', container_id, 'ip', 'link', + 'set', if_remote_name, 'address', vif['address'], + run_as_root=True) + if ipv6_address: + ip = ipv6_address + "/" + ipv6_netmask + gateway = ipv6_gateway + utils.execute('ip', 'netns', 'exec', container_id, 'ifconfig', + if_remote_name, 'inet6', 'add', ip, + run_as_root=True) + utils.execute('ip', 'netns', 'exec', container_id, 'ip', '-6', + 'route', 'replace', 'default', 'via', gateway, + 'dev', if_remote_name, run_as_root=True) + if ipv4_address != '0.0.0.0': + ip = ipv4_address + "/" + ipv4_netmask + gateway = ipv4_gateway + utils.execute('ip', 'netns', 'exec', container_id, 'ifconfig', + if_remote_name, ip, run_as_root=True) + utils.execute('ip', 'netns', 'exec', container_id, 'ip', + 'route', 'replace', 'default', 'via', gateway, + 'dev', if_remote_name, run_as_root=True) + utils.execute('ip', 'netns', 'exec', container_id, 'ip', 'link', + 'set', if_remote_name, 'up', run_as_root=True) + except Exception: + LOG.exception(_("Failed to attach vif"), instance=instance) + + def _retrieve_ip_address(self, vif, version): + address = None + netmask = None + gateway = None + if 'subnets' in vif['network']: + subnets = vif['network']['subnets'] + for subnet in subnets: + ips = subnet['ips'][0] + if (ips['version'] == version): + if ips['address'] is not None: + address = ips['address'] + netmask = subnet['cidr'].split('/')[1] + gateway = subnet['gateway']['address'] + + return address, netmask, gateway + + def unplug(self, instance, vif): + vif_type = vif['type'] + if_local_name = 'veth%s' % vif['id'][:8] + + LOG.debug('Unplug vif_type=%(vif_type)s instance=%(instance)s ' + 'vif=%(vif)s', + {'vif_type': vif_type, 'instance': instance, + 'vif': vif}) + + try: + self._vrouter_client.delete_port(vif['id']) + if linux_net.device_exists(if_local_name): + utils.execute('ip', 'link', 'delete', if_local_name, + run_as_root=True) + except Exception: + LOG.exception(_("Delete port failed"), instance=instance) diff --git a/nova/virt/docker/vifs.py b/nova/virt/docker/vifs.py new file mode 100644 index 000000000..02bd49e81 --- /dev/null +++ b/nova/virt/docker/vifs.py @@ -0,0 +1,492 @@ +# Copyright (C) 2013 VMware, Inc +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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. + + +from oslo_concurrency import processutils +from oslo_log import log as logging + +from nova import exception +from nova.i18n import _ +from nova.network import linux_net +from nova.network import manager +from nova.network import model as network_model +from nova import utils +from nova.virt.docker import network +from oslo_config import cfg +import random + +# We need config opts from manager, but pep8 complains, this silences it. +assert manager + +CONF = cfg.CONF +CONF.import_opt('my_ip', 'nova.conf.netconf') +CONF.import_opt('vlan_interface', 'nova.manager') +CONF.import_opt('flat_interface', 'nova.manager') + +LOG = logging.getLogger(__name__) + + +class DockerGenericVIFDriver(object): + + def plug(self, instance, vif): + vif_type = vif['type'] + + LOG.debug('plug vif_type=%(vif_type)s instance=%(instance)s ' + 'vif=%(vif)s', + {'vif_type': vif_type, 'instance': instance, + 'vif': vif}) + + if vif_type is None: + raise exception.NovaException( + _("vif_type parameter must be present " + "for this vif_driver implementation")) + + if vif_type == network_model.VIF_TYPE_BRIDGE: + self.plug_bridge(instance, vif) + elif vif_type == network_model.VIF_TYPE_OVS: + if self.ovs_hybrid_required(vif): + self.plug_ovs_hybrid(instance, vif) + else: + self.plug_ovs(instance, vif) + elif vif_type == network_model.VIF_TYPE_MIDONET: + self.plug_midonet(instance, vif) + elif vif_type == network_model.VIF_TYPE_IOVISOR: + self.plug_iovisor(instance, vif) + elif vif_type == 'hyperv': + self.plug_windows(instance, vif) + else: + raise exception.NovaException( + _("Unexpected vif_type=%s") % vif_type) + + def plug_windows(self, instance, vif): + pass + + def plug_iovisor(self, instance, vif): + """Plug docker vif into IOvisor + + Creates a port on IOvisor and onboards the interface + """ + if_local_name = 'tap%s' % vif['id'][:11] + if_remote_name = 'ns%s' % vif['id'][:11] + + iface_id = vif['id'] + net_id = vif['network']['id'] + tenant_id = instance['project_id'] + + # Device already exists so return. + if linux_net.device_exists(if_local_name): + return + undo_mgr = utils.UndoManager() + + try: + utils.execute('ip', 'link', 'add', 'name', if_local_name, 'type', + 'veth', 'peer', 'name', if_remote_name, + run_as_root=True) + utils.execute('ifc_ctl', 'gateway', 'add_port', if_local_name, + run_as_root=True) + utils.execute('ifc_ctl', 'gateway', 'ifup', if_local_name, + 'access_vm', + vif['network']['label'] + "_" + iface_id, + vif['address'], 'pgtag2=%s' % net_id, + 'pgtag1=%s' % tenant_id, run_as_root=True) + utils.execute('ip', 'link', 'set', if_local_name, 'up', + run_as_root=True) + + except Exception: + LOG.exception("Failed to configure network on IOvisor") + msg = _('Failed to setup the network, rolling back') + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + def plug_ovs(self, instance, vif): + if_local_name = 'tap%s' % vif['id'][:11] + if_remote_name = 'ns%s' % vif['id'][:11] + bridge = vif['network']['bridge'] + + # Device already exists so return. + if linux_net.device_exists(if_local_name): + return + undo_mgr = utils.UndoManager() + + try: + utils.execute('ip', 'link', 'add', 'name', if_local_name, 'type', + 'veth', 'peer', 'name', if_remote_name, + run_as_root=True) + linux_net.create_ovs_vif_port(bridge, if_local_name, + network.get_ovs_interfaceid(vif), + vif['address'], + instance['uuid']) + utils.execute('ip', 'link', 'set', if_local_name, 'up', + run_as_root=True) + except Exception: + LOG.exception("Failed to configure network") + msg = _('Failed to setup the network, rolling back') + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + def plug_midonet(self, instance, vif): + """Plug into MidoNet's network port + + This accomplishes binding of the vif to a MidoNet virtual port + """ + if_local_name = 'tap%s' % vif['id'][:11] + if_remote_name = 'ns%s' % vif['id'][:11] + port_id = network.get_ovs_interfaceid(vif) + + # Device already exists so return. + if linux_net.device_exists(if_local_name): + return + + undo_mgr = utils.UndoManager() + try: + utils.execute('ip', 'link', 'add', 'name', if_local_name, 'type', + 'veth', 'peer', 'name', if_remote_name, + run_as_root=True) + undo_mgr.undo_with(lambda: utils.execute( + 'ip', 'link', 'delete', if_local_name, run_as_root=True)) + utils.execute('ip', 'link', 'set', if_local_name, 'up', + run_as_root=True) + utils.execute('mm-ctl', '--bind-port', port_id, if_local_name, + run_as_root=True) + except Exception: + LOG.exception("Failed to configure network") + msg = _('Failed to setup the network, rolling back') + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + def plug_ovs_hybrid(self, instance, vif): + """Plug using hybrid strategy + + Create a per-VIF linux bridge, then link that bridge to the OVS + integration bridge via a veth device, setting up the other end + of the veth device just like a normal OVS port. Then boot the + VIF on the linux bridge. and connect the tap port to linux bridge + """ + + if_local_name = 'tap%s' % vif['id'][:11] + if_remote_name = 'ns%s' % vif['id'][:11] + iface_id = self.get_ovs_interfaceid(vif) + br_name = self.get_br_name(vif['id']) + v1_name, v2_name = self.get_veth_pair_names(vif['id']) + + # Device already exists so return. + if linux_net.device_exists(if_local_name): + return + undo_mgr = utils.UndoManager() + + try: + if not linux_net.device_exists(br_name): + utils.execute('brctl', 'addbr', br_name, run_as_root=True) + # Incase of failure undo the Steps + # Deleting/Undoing the interface will delete all + # associated resources + undo_mgr.undo_with(lambda: utils.execute( + 'brctl', 'delbr', br_name, run_as_root=True)) + # LOG.exception('Throw Test exception with bridgename %s' + # % br_name) + + utils.execute('brctl', 'setfd', br_name, 0, run_as_root=True) + utils.execute('brctl', 'stp', br_name, 'off', run_as_root=True) + utils.execute('tee', + ('/sys/class/net/%s/bridge/multicast_snooping' % + br_name), + process_input='0', + run_as_root=True, + check_exit_code=[0, 1]) + + if not linux_net.device_exists(v2_name): + linux_net._create_veth_pair(v1_name, v2_name) + undo_mgr.undo_with(lambda: utils.execute( + 'ip', 'link', 'delete', v1_name, run_as_root=True)) + + utils.execute('ip', 'link', 'set', br_name, 'up', + run_as_root=True) + undo_mgr.undo_with(lambda: utils.execute('ip', 'link', 'set', + br_name, 'down', + run_as_root=True)) + + # Deleting/Undoing the interface will delete all + # associated resources (remove from the bridge, its + # pair, etc...) + utils.execute('brctl', 'addif', br_name, v1_name, + run_as_root=True) + + linux_net.create_ovs_vif_port(self.get_bridge_name(vif), + v2_name, + iface_id, vif['address'], + instance['uuid']) + undo_mgr.undo_with( + lambda: utils.execute('ovs-vsctl', 'del-port', + self.get_bridge_name(vif), + v2_name, run_as_root=True)) + + utils.execute('ip', 'link', 'add', 'name', if_local_name, 'type', + 'veth', 'peer', 'name', if_remote_name, + run_as_root=True) + undo_mgr.undo_with( + lambda: utils.execute('ip', 'link', 'delete', if_local_name, + run_as_root=True)) + + # Deleting/Undoing the interface will delete all + # associated resources (remove from the bridge, its pair, etc...) + utils.execute('brctl', 'addif', br_name, if_local_name, + run_as_root=True) + utils.execute('ip', 'link', 'set', if_local_name, 'up', + run_as_root=True) + except Exception: + msg = "Failed to configure Network." \ + " Rolling back the network interfaces %s %s %s %s " % ( + br_name, if_local_name, v1_name, v2_name) + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + # We are creating our own mac's now because the linux bridge interface + # takes on the lowest mac that is assigned to it. By using FE range + # mac's we prevent the interruption and possible loss of networking + # from changing mac addresses. + def _fe_random_mac(self): + mac = [0xfe, 0xed, + random.randint(0x00, 0xff), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(map(lambda x: "%02x" % x, mac)) + + def plug_bridge(self, instance, vif): + if_local_name = 'tap%s' % vif['id'][:11] + if_remote_name = 'ns%s' % vif['id'][:11] + bridge = vif['network']['bridge'] + gateway = network.find_gateway(instance, vif['network']) + + net = vif['network'] + if net.get_meta('should_create_vlan', False): + vlan = net.get_meta('vlan'), + iface = (CONF.vlan_interface or + vif['network'].get_meta('bridge_interface')) + linux_net.LinuxBridgeInterfaceDriver.ensure_vlan_bridge( + vlan, + bridge, + iface, + net_attrs=vif, + mtu=vif.get('mtu')) + iface = 'vlan%s' % vlan + else: + iface = (CONF.flat_interface or + vif['network'].get_meta('bridge_interface')) + LOG.debug('Ensuring bridge for %s - %s' % (iface, bridge)) + linux_net.LinuxBridgeInterfaceDriver.ensure_bridge( + bridge, + iface, + net_attrs=vif, + gateway=gateway) + + # Device already exists so return. + if linux_net.device_exists(if_local_name): + return + undo_mgr = utils.UndoManager() + + try: + utils.execute('ip', 'link', 'add', 'name', if_local_name, 'type', + 'veth', 'peer', 'name', if_remote_name, + run_as_root=True) + undo_mgr.undo_with(lambda: utils.execute( + 'ip', 'link', 'delete', if_local_name, run_as_root=True)) + # NOTE(samalba): Deleting the interface will delete all + # associated resources (remove from the bridge, its pair, etc...) + utils.execute('ip', 'link', 'set', if_local_name, 'address', + self._fe_random_mac(), run_as_root=True) + utils.execute('brctl', 'addif', bridge, if_local_name, + run_as_root=True) + utils.execute('ip', 'link', 'set', if_local_name, 'up', + run_as_root=True) + except Exception: + LOG.exception("Failed to configure network") + msg = _('Failed to setup the network, rolling back') + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + def unplug(self, instance, vif): + vif_type = vif['type'] + + LOG.debug('vif_type=%(vif_type)s instance=%(instance)s ' + 'vif=%(vif)s', + {'vif_type': vif_type, 'instance': instance, + 'vif': vif}) + + if vif_type is None: + raise exception.NovaException( + _("vif_type parameter must be present " + "for this vif_driver implementation")) + + if vif_type == network_model.VIF_TYPE_BRIDGE: + self.unplug_bridge(instance, vif) + elif vif_type == network_model.VIF_TYPE_OVS: + if self.ovs_hybrid_required(vif): + self.unplug_ovs_hybrid(instance, vif) + else: + self.unplug_ovs(instance, vif) + elif vif_type == network_model.VIF_TYPE_MIDONET: + self.unplug_midonet(instance, vif) + elif vif_type == network_model.VIF_TYPE_IOVISOR: + self.unplug_iovisor(instance, vif) + elif vif_type == 'hyperv': + self.unplug_windows(instance, vif) + else: + raise exception.NovaException( + _("Unexpected vif_type=%s") % vif_type) + + def unplug_windows(self, instance, vif): + pass + + def unplug_iovisor(self, instance, vif): + """Unplug vif from IOvisor + + Offboard an interface and deletes port from IOvisor + """ + if_local_name = 'tap%s' % vif['id'][:11] + iface_id = vif['id'] + try: + utils.execute('ifc_ctl', 'gateway', 'ifdown', + if_local_name, 'access_vm', + vif['network']['label'] + "_" + iface_id, + vif['address'], run_as_root=True) + utils.execute('ifc_ctl', 'gateway', 'del_port', if_local_name, + run_as_root=True) + linux_net.delete_net_dev(if_local_name) + except processutils.ProcessExecutionError: + LOG.exception(_("Failed while unplugging vif"), instance=instance) + + def unplug_ovs(self, instance, vif): + """Unplug the VIF by deleting the port from the bridge.""" + try: + linux_net.delete_ovs_vif_port(vif['network']['bridge'], + vif['devname']) + except processutils.ProcessExecutionError: + LOG.exception(_("Failed while unplugging vif"), instance=instance) + + def unplug_midonet(self, instance, vif): + """Unplug into MidoNet's network port + + This accomplishes unbinding of the vif from its MidoNet virtual port + """ + try: + utils.execute('mm-ctl', '--unbind-port', + network.get_ovs_interfaceid(vif), run_as_root=True) + except processutils.ProcessExecutionError: + LOG.exception(_("Failed while unplugging vif"), instance=instance) + + def unplug_ovs_hybrid(self, instance, vif): + """UnPlug using hybrid strategy + + Unhook port from OVS, unhook port from bridge, delete + bridge, and delete both veth devices. + """ + try: + br_name = self.get_br_name(vif['id']) + v1_name, v2_name = self.get_veth_pair_names(vif['id']) + + if linux_net.device_exists(br_name): + utils.execute('brctl', 'delif', br_name, v1_name, + run_as_root=True) + utils.execute('ip', 'link', 'set', br_name, 'down', + run_as_root=True) + utils.execute('brctl', 'delbr', br_name, + run_as_root=True) + + linux_net.delete_ovs_vif_port(self.get_bridge_name(vif), + v2_name) + except processutils.ProcessExecutionError: + LOG.exception(_("Failed while unplugging vif"), instance=instance) + + def unplug_bridge(self, instance, vif): + # NOTE(arosen): nothing has to be done in the linuxbridge case + # as when the veth is deleted it automatically is removed from + # the bridge. + pass + + def attach(self, instance, vif, container_id): + vif_type = vif['type'] + if_remote_name = 'ns%s' % vif['id'][:11] + gateways = network.find_gateways(instance, vif['network']) + ips = network.find_fixed_ips(instance, vif['network']) + + LOG.debug('attach vif_type=%(vif_type)s instance=%(instance)s ' + 'vif=%(vif)s', + {'vif_type': vif_type, 'instance': instance, + 'vif': vif}) + + try: + utils.execute('ip', 'link', 'set', if_remote_name, 'netns', + container_id, run_as_root=True) + utils.execute('ip', 'netns', 'exec', container_id, 'ip', 'link', + 'set', if_remote_name, 'address', vif['address'], + run_as_root=True) + for ip in ips: + utils.execute('ip', 'netns', 'exec', container_id, 'ip', + 'addr', 'add', ip, 'dev', if_remote_name, + run_as_root=True) + utils.execute('ip', 'netns', 'exec', container_id, 'ip', 'link', + 'set', if_remote_name, 'up', run_as_root=True) + + # Setup MTU on if_remote_name is required if it is a non + # default value + mtu = None + if vif.get('mtu') is not None: + mtu = vif.get('mtu') + if mtu is not None: + utils.execute('ip', 'netns', 'exec', container_id, 'ip', + 'link', 'set', if_remote_name, 'mtu', mtu, + run_as_root=True) + + for gateway in gateways: + utils.execute('ip', 'netns', 'exec', container_id, + 'ip', 'route', 'replace', 'default', 'via', + gateway, 'dev', if_remote_name, run_as_root=True) + + # Disable TSO, for now no config option + utils.execute('ip', 'netns', 'exec', container_id, 'ethtool', + '--offload', if_remote_name, 'tso', 'off', + run_as_root=True) + + except Exception: + LOG.exception("Failed to attach vif") + + def get_bridge_name(self, vif): + return vif['network']['bridge'] + + def get_ovs_interfaceid(self, vif): + return vif.get('ovs_interfaceid') or vif['id'] + + def get_br_name(self, iface_id): + return ("qbr" + iface_id)[:network_model.NIC_NAME_LEN] + + def get_veth_pair_names(self, iface_id): + return (("qvb%s" % iface_id)[:network_model.NIC_NAME_LEN], + ("qvo%s" % iface_id)[:network_model.NIC_NAME_LEN]) + + def ovs_hybrid_required(self, vif): + ovs_hybrid_required = self.get_firewall_required(vif) or \ + self.get_hybrid_plug_enabled(vif) + return ovs_hybrid_required + + def get_firewall_required(self, vif): + if vif.get('details'): + enabled = vif['details'].get('port_filter', False) + if enabled: + return False + if CONF.firewall_driver != "nova.virt.firewall.NoopFirewallDriver": + return True + return False + + def get_hybrid_plug_enabled(self, vif): + if vif.get('details'): + return vif['details'].get('ovs_hybrid_plug', False) + return False diff --git a/nova/virt/hostutils.py b/nova/virt/hostutils.py new file mode 100644 index 000000000..fb3b4a370 --- /dev/null +++ b/nova/virt/hostutils.py @@ -0,0 +1,35 @@ +# Copyright (c) 2014 Docker, Inc. +# All Rights Reserved. +# +# 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. + + +import ctypes +import datetime +import os +import time + +from nova import utils + + +def sys_uptime(): + """Returns the host uptime.""" + + if os.name == 'nt': + tick_count64 = ctypes.windll.kernel32.GetTickCount64() + return ("%s up %s, 0 users, load average: 0, 0, 0" % + (str(time.strftime("%H:%M:%S")), + str(datetime.timedelta(milliseconds=long(tick_count64))))) + else: + out, err = utils.execute('env', 'LANG=C', 'uptime') + return out diff --git a/requirements.txt b/requirements.txt index 6f31e0f97..c29edfd2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ pbr>=1.8 # Apache-2.0 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD python-etcd>=0.4.3 # MIT License python-glanceclient>=2.5.0 # Apache-2.0 +python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 oslo.log>=3.11.0 # Apache-2.0 oslo.concurrency>=3.8.0 # Apache-2.0 diff --git a/zun/common/clients.py b/zun/common/clients.py index 08f642e14..50e82cc2e 100644 --- a/zun/common/clients.py +++ b/zun/common/clients.py @@ -13,6 +13,7 @@ # under the License. from glanceclient import client as glanceclient +from novaclient import client as novaclient from oslo_log import log as logging from zun.common import exception @@ -29,6 +30,7 @@ class OpenStackClients(object): self.context = context self._keystone = None self._glance = None + self._nova = None def url_for(self, **kwargs): return self.keystone().session.get_endpoint(**kwargs) @@ -83,3 +85,14 @@ class OpenStackClients(object): self._glance = glanceclient.Client(glanceclient_version, **args) return self._glance + + @exception.wrap_keystone_exception + def nova(self): + if self._nova: + return self._nova + + nova_api_version = self._get_client_option('nova', 'api_version') + session = self.keystone().session + self._nova = novaclient.Client(nova_api_version, session=session) + + return self._nova diff --git a/zun/common/exception.py b/zun/common/exception.py index 1cdf35eeb..a3cf29c1f 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -368,3 +368,17 @@ class DockerError(ZunException): class PollTimeOut(ZunException): message = _("Polling request timed out.") + + +class ServerInError(ZunException): + message = _('Went to status %(resource_status)s due to ' + '"%(status_reason)s"') + + +class ServerUnknownStatus(ZunException): + message = _('%(result)s - Unknown status %(resource_status)s due to ' + '"%(status_reason)s"') + + +class EntityNotFound(ZunException): + message = _("The %(entity)s (%(name)s) could not be found.") diff --git a/zun/common/nova.py b/zun/common/nova.py new file mode 100644 index 000000000..b2f7cc33f --- /dev/null +++ b/zun/common/nova.py @@ -0,0 +1,250 @@ +# +# 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. + +import requests +import six + +from novaclient import exceptions +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import uuidutils + +from zun.common import clients +from zun.common import exception +from zun.common.i18n import _ +from zun.common.i18n import _LW + + +LOG = logging.getLogger(__name__) + + +def retry_if_connection_err(exception): + return isinstance(exception, requests.ConnectionError) + + +class NovaClient(object): + + deferred_server_statuses = ['BUILD', + 'HARD_REBOOT', + 'PASSWORD', + 'REBOOT', + 'RESCUE', + 'RESIZE', + 'REVERT_RESIZE', + 'SHUTOFF', + 'SUSPENDED', + 'VERIFY_RESIZE'] + + def __init__(self, context): + self.context = context + self._client = None + + def client(self): + if not self._client: + self._client = clients.OpenStackClients(self.context).nova() + return self._client + + def is_not_found(self, ex): + return isinstance(ex, exceptions.NotFound) + + def create_server(self, name, image, flavor, **kwargs): + image = self.get_image_by_name_or_id(image) + flavor = self.get_flavor_by_name_or_id(flavor) + return self.client().servers.create(name, image, flavor, **kwargs) + + def get_image_by_name_or_id(self, image_ident): + """Get an image by name or ID.""" + try: + return self.client().glance.find_image(image_ident) + except exceptions.NotFound as e: + raise exception.ImageNotFound(six.text_type(e)) + except exceptions.NoUniqueMatch as e: + raise exception.Conflict(six.text_type(e)) + + def get_flavor_by_name_or_id(self, flavor_ident): + """Get the flavor object for the specified flavor name or id. + + :param flavor_identifier: the name or id of the flavor to find + :returns: a flavor object with name or id :flavor: + """ + try: + flavor = self.client().flavors.get(flavor_ident) + except exceptions.NotFound: + flavor = self.client().flavors.find(name=flavor_ident) + + return flavor + + def fetch_server(self, server_id): + """Fetch fresh server object from Nova. + + Log warnings and return None for non-critical API errors. + Use this method in various ``check_*_complete`` resource methods, + where intermittent errors can be tolerated. + """ + server = None + try: + server = self.client().servers.get(server_id) + except exceptions.OverLimit as exc: + LOG.warning(_LW("Received an OverLimit response when " + "fetching server (%(id)s) : %(exception)s"), + {'id': server_id, + 'exception': exc}) + except exceptions.ClientException as exc: + if ((getattr(exc, 'http_status', getattr(exc, 'code', None)) in + (500, 503))): + LOG.warning(_LW("Received the following exception when " + "fetching server (%(id)s) : %(exception)s"), + {'id': server_id, + 'exception': exc}) + else: + raise + return server + + def refresh_server(self, server): + """Refresh server's attributes. + + Also log warnings for non-critical API errors. + """ + try: + server.get() + except exceptions.OverLimit as exc: + LOG.warning(_LW("Server %(name)s (%(id)s) received an OverLimit " + "response during server.get(): %(exception)s"), + {'name': server.name, + 'id': server.id, + 'exception': exc}) + except exceptions.ClientException as exc: + if ((getattr(exc, 'http_status', getattr(exc, 'code', None)) in + (500, 503))): + LOG.warning(_LW('Server "%(name)s" (%(id)s) received the ' + 'following exception during server.get(): ' + '%(exception)s'), + {'name': server.name, + 'id': server.id, + 'exception': exc}) + else: + raise + + def get_status(self, server): + """Return the server's status. + + :param server: server object + :returns: status as a string + """ + # Some clouds append extra (STATUS) strings to the status, strip it + return server.status.split('(')[0] + + def check_active(self, server): + """Check server status. + + Accepts both server IDs and server objects. + Returns True if server is ACTIVE, + raises errors when server has an ERROR or unknown to Zun status, + returns False otherwise. + + """ + # not checking with is_uuid_like as most tests use strings e.g. '1234' + if isinstance(server, six.string_types): + server = self.fetch_server(server) + if server is None: + return False + else: + status = self.get_status(server) + else: + status = self.get_status(server) + if status != 'ACTIVE': + self.refresh_server(server) + status = self.get_status(server) + + if status in self.deferred_server_statuses: + return False + elif status == 'ACTIVE': + return True + elif status == 'ERROR': + fault = getattr(server, 'fault', {}) + raise exception.ServerInError( + resource_status=status, + status_reason=_("Message: %(message)s, Code: %(code)s") % { + 'message': fault.get('message', _('Unknown')), + 'code': fault.get('code', _('Unknown')) + }) + else: + raise exception.ServerUnknownStatus( + resource_status=server.status, + status_reason=_('Unknown'), + result=_('Server is not active')) + + def delete_server(self, server): + server_id = self.get_server_id(server, raise_on_error=False) + if server_id: + self.client().servers.delete(server_id) + return server_id + + def check_delete_server_complete(self, server_id): + """Wait for server to disappear from Nova.""" + try: + server = self.fetch_server(server_id) + except Exception as exc: + self.ignore_not_found(exc) + return True + if not server: + return False + task_state_in_nova = getattr(server, 'OS-EXT-STS:task_state', None) + # the status of server won't change until the delete task has done + if task_state_in_nova == 'deleting': + return False + + status = self.get_status(server) + if status in ("DELETED", "SOFT_DELETED"): + return True + if status == 'ERROR': + fault = getattr(server, 'fault', {}) + message = fault.get('message', 'Unknown') + code = fault.get('code') + errmsg = _("Server %(name)s delete failed: (%(code)s) " + "%(message)s") % dict(name=server.name, + code=code, + message=message) + raise exception.ServerInError(resource_status=status, + status_reason=errmsg) + return False + + @excutils.exception_filter + def ignore_not_found(self, ex): + """Raises the exception unless it is a not-found.""" + return self.is_not_found(ex) + + def get_addresses(self, server): + """Return the server's IP address, fetching it from Nova.""" + try: + server_id = self.get_server_id(server) + server = self.client().servers.get(server_id) + except exceptions.NotFound as ex: + LOG.warning(_LW('Instance (%(server)s) not found: %(ex)s'), + {'server': server, 'ex': ex}) + else: + return server.addresses + + def get_server_id(self, server, raise_on_error=True): + if uuidutils.is_uuid_like(server): + return server + elif isinstance(server, six.string_types): + servers = self.client().servers.list(search_opts={'name': server}) + if len(servers) == 1: + return servers[0].id + + if raise_on_error: + raise exception.ZunException(_( + "Unable to get server id with name %s") % server) + else: + raise exception.ZunException(_("Unexpected server type")) diff --git a/zun/common/utils.py b/zun/common/utils.py index d41a4e1ed..d1af9c4b3 100644 --- a/zun/common/utils.py +++ b/zun/common/utils.py @@ -18,10 +18,12 @@ import eventlet import functools import mimetypes -from oslo_utils import uuidutils +import time from oslo_context import context as common_context from oslo_log import log as logging +from oslo_service import loopingcall +from oslo_utils import uuidutils import pecan import six @@ -143,6 +145,36 @@ def check_container_id(function): return decorated_function +def poll_until(retriever, condition=lambda value: value, + sleep_time=1, time_out=None, success_msg=None, + timeout_msg=None): + """Retrieves object until it passes condition, then returns it. + + If time_out_limit is passed in, PollTimeOut will be raised once that + amount of time is elapsed. + """ + start_time = time.time() + + def poll_and_check(): + obj = retriever() + if condition(obj): + raise loopingcall.LoopingCallDone(retvalue=obj) + if time_out is not None and time.time() - start_time > time_out: + raise exception.PollTimeOut + + try: + poller = loopingcall.FixedIntervalLoopingCall( + f=poll_and_check).start(sleep_time, initial_delay=False) + poller.wait() + LOG.info(success_msg) + except exception.PollTimeOut: + LOG.error(timeout_msg) + raise + except Exception: + LOG.exception(_("Unexpected exception occurred.")) + raise + + def get_image_pull_policy(image_pull_policy, image_tag): if not image_pull_policy: if image_tag == 'latest': diff --git a/zun/conf/__init__.py b/zun/conf/__init__.py index 0e9a63d8f..cb4dd3f25 100644 --- a/zun/conf/__init__.py +++ b/zun/conf/__init__.py @@ -21,6 +21,7 @@ from zun.conf import database from zun.conf import docker from zun.conf import glance_client from zun.conf import image_driver +from zun.conf import nova_client from zun.conf import path from zun.conf import services from zun.conf import zun_client @@ -34,6 +35,7 @@ database.register_opts(CONF) docker.register_opts(CONF) glance_client.register_opts(CONF) image_driver.register_opts(CONF) +nova_client.register_opts(CONF) path.register_opts(CONF) services.register_opts(CONF) zun_client.register_opts(CONF) diff --git a/zun/conf/container_driver.py b/zun/conf/container_driver.py index 86814181a..8967ea6da 100644 --- a/zun/conf/container_driver.py +++ b/zun/conf/container_driver.py @@ -20,6 +20,7 @@ driver_opts = [ Possible values: * ``docker.driver.DockerDriver`` +* ``docker.driver.NovaDockerDriver`` Services which consume this: @@ -29,6 +30,8 @@ Interdependencies to other options: * None """), + cfg.IntOpt('default_sleep_time', default=1, + help='Time to sleep (in seconds) during waiting for an event.'), cfg.IntOpt('default_timeout', default=60 * 10, help='Maximum time (in seconds) to wait for an event.'), ] diff --git a/zun/conf/nova_client.py b/zun/conf/nova_client.py new file mode 100644 index 000000000..f24319ff2 --- /dev/null +++ b/zun/conf/nova_client.py @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 eNovance +# +# 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. + +from oslo_config import cfg + +from zun.common.i18n import _ + + +nova_group = cfg.OptGroup(name='nova_client', + title='Options for the Nova client') + +common_security_opts = [ + cfg.StrOpt('ca_file', + help=_('Optional CA cert file to use in SSL connections.')), + cfg.StrOpt('cert_file', + help=_('Optional PEM-formatted certificate chain file.')), + cfg.StrOpt('key_file', + help=_('Optional PEM-formatted file that contains the ' + 'private key.')), + cfg.BoolOpt('insecure', + default=False, + help=_("If set, then the server's certificate will not " + "be verified."))] + +nova_client_opts = [ + cfg.StrOpt('api_version', + default='2.37', + help=_('Version of Nova API to use in novaclient.'))] + + +ALL_OPTS = (nova_client_opts + common_security_opts) + + +def register_opts(conf): + conf.register_group(nova_group) + conf.register_opts(ALL_OPTS, group=nova_group) + + +def list_opts(): + return {nova_group: ALL_OPTS} diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index b811bc7ec..9acba6c6a 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -16,7 +16,12 @@ import six from docker import errors from oslo_log import log as logging +from zun.common import exception +from zun.common.i18n import _LE +from zun.common.i18n import _LI from zun.common.i18n import _LW +from zun.common import nova +from zun.common import utils from zun.common.utils import check_container_id import zun.conf from zun.container.docker import utils as docker_utils @@ -61,6 +66,7 @@ class DockerDriver(driver.ContainerDriver): % (image, name)) kwargs = { + 'name': self.get_container_name(container), 'hostname': container.hostname, 'command': container.command, 'environment': container.environment, @@ -230,7 +236,10 @@ class DockerDriver(driver.ContainerDriver): container.meta['sandbox_id'] = id def get_sandbox_name(self, container): - return 'sandbox-' + container.uuid + return 'zun-sandbox-' + container.uuid + + def get_container_name(self, container): + return 'zun-' + container.uuid def get_addresses(self, context, container): sandbox_id = self.get_sandbox_id(container) @@ -245,3 +254,85 @@ class DockerDriver(driver.ContainerDriver): ], } return addresses + + +class NovaDockerDriver(DockerDriver): + def create_sandbox(self, context, container, key_name=None, + flavor='m1.small', image='kubernetes/pause', + nics='auto'): + name = self.get_sandbox_name(container) + novaclient = nova.NovaClient(context) + sandbox = novaclient.create_server(name=name, image=image, + flavor=flavor, key_name=key_name, + nics=nics) + self._ensure_active(novaclient, sandbox) + sandbox_id = self._find_container_by_server_name(name) + return sandbox_id + + def _ensure_active(self, novaclient, server, timeout=300): + '''Wait until the Nova instance to become active.''' + def _check_active(): + return novaclient.check_active(server) + + success_msg = _LI("Created server %s successfully.") % server.id + timeout_msg = _LE("Failed to create server %s. Timeout waiting for " + "server to become active.") % server.id + utils.poll_until(_check_active, + sleep_time=CONF.default_sleep_time, + time_out=timeout or CONF.default_timeout, + success_msg=success_msg, timeout_msg=timeout_msg) + + def delete_sandbox(self, context, sandbox_id): + novaclient = nova.NovaClient(context) + server_name = self._find_server_by_container_id(sandbox_id) + if not server_name: + LOG.warning(_LW("Cannot find server name for sandbox %s") % + sandbox_id) + return + + server_id = novaclient.delete_server(server_name) + self._ensure_deleted(novaclient, server_id) + + def _ensure_deleted(self, novaclient, server_id, timeout=300): + '''Wait until the Nova instance to be deleted.''' + def _check_delete_complete(): + return novaclient.check_delete_server_complete(server_id) + + success_msg = _LI("Delete server %s successfully.") % server_id + timeout_msg = _LE("Failed to create server %s. Timeout waiting for " + "server to be deleted.") % server_id + utils.poll_until(_check_delete_complete, + sleep_time=CONF.default_sleep_time, + time_out=timeout or CONF.default_timeout, + success_msg=success_msg, timeout_msg=timeout_msg) + + def get_addresses(self, context, container): + novaclient = nova.NovaClient(context) + sandbox_id = self.get_sandbox_id(container) + if sandbox_id: + server_name = self._find_server_by_container_id(sandbox_id) + if server_name: + # TODO(hongbin): Standardize the format of addresses + return novaclient.get_addresses(server_name) + else: + return None + else: + return None + + def _find_container_by_server_name(self, name): + with docker_utils.docker_client() as docker: + for info in docker.list_instances(inspect=True): + if info['Config'].get('Hostname') == name: + return info['Id'] + raise exception.ZunException(_( + "Cannot find container with name %s") % name) + + def _find_server_by_container_id(self, container_id): + with docker_utils.docker_client() as docker: + try: + info = docker.inspect_container(container_id) + return info['Config'].get('Hostname') + except errors.APIError as e: + if e.response.status_code != 404: + raise + return None diff --git a/zun/container/driver.py b/zun/container/driver.py index cec5ce735..7f462ed32 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -126,6 +126,10 @@ class ContainerDriver(object): """Retrieve sandbox name.""" raise NotImplementedError() + def get_container_name(self, container): + """Retrieve sandbox name.""" + raise NotImplementedError() + def get_addresses(self, context, container): """Retrieve IP addresses of the container.""" raise NotImplementedError()