diff --git a/Makefile b/Makefile
index 087bbdd0..010269e3 100644
--- a/Makefile
+++ b/Makefile
@@ -12,11 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+BUILD_DIR := $(shell mktemp -d)
DOCKER_REGISTRY ?= quay.io
IMAGE_NAME ?= drydock
IMAGE_PREFIX ?= attcomdev
IMAGE_TAG ?= latest
-HELM ?= helm
+HELM := $(BUILD_DIR)/helm
PROXY ?= http://one.proxy.att.com:8080
USE_PROXY ?= false
PUSH_IMAGE ?= false
@@ -34,21 +35,36 @@ run_images: run_drydock
# Run tests
.PHONY: tests
-tests: coverage_test
+tests: external_dep pep8 security docs unit_tests
+
+# Intall external (not managed by tox/pip) dependencies
+.PHONY: external_dep
+external_dep:
+ sudo ./hostdeps.sh
# Run unit and Postgres integration tests in coverage mode
.PHONY: coverage_test
-coverage_test: build_drydock
- tox -e coverage
+coverage_test: build_drydock external_dep
+ tox -re coverage
+
+# Run just unit tests
+.PHONY: unit_tests
+unit_tests:
+ tox -re unit
# Run the drydock container and exercise simple tests
.PHONY: run_drydock
run_drydock: build_drydock
tools/drydock_image_run.sh
+# It seems CICD expects the target 'drydock' to
+# build the chart
+.PHONY: drydock
+drydock: charts
+
# Create tgz of the chart
.PHONY: charts
-charts: clean
+charts: clean helm-init
$(HELM) dep up charts/drydock
$(HELM) package charts/drydock
@@ -58,18 +74,27 @@ lint: pep8 helm_lint
# Dry run templating of chart
.PHONY: dry-run
-dry-run: clean
- tools/helm_tk.sh $(HELM)
+dry-run: clean helm-init
$(HELM) template --set manifests.secret_ssh_key=true --set conf.ssh.private_key=foo charts/drydock
+# Initialize local helm config
+.PHONY: helm-init
+helm-init: helm-install
+ tools/helm_tk.sh $(HELM)
+
+# Install helm binary
+.PHONY: helm-install
+helm-install:
+ tools/helm_install.sh $(HELM)
+
# Make targets intended for use by the primary targets above.
.PHONY: build_drydock
build_drydock:
ifeq ($(USE_PROXY), true)
- docker build -t $(IMAGE) --label $(LABEL) -f images/drydock/Dockerfile . --build-arg http_proxy=$(PROXY) --build-arg https_proxy=$(PROXY)
+ docker build --network host -t $(IMAGE) --label $(LABEL) -f images/drydock/Dockerfile . --build-arg http_proxy=$(PROXY) --build-arg https_proxy=$(PROXY)
else
- docker build -t $(IMAGE) --label $(LABEL) -f images/drydock/Dockerfile .
+ docker build --network host -t $(IMAGE) --label $(LABEL) -f images/drydock/Dockerfile .
endif
ifeq ($(PUSH_IMAGE), true)
docker push $(IMAGE)
@@ -78,12 +103,17 @@ endif
.PHONY: docs
docs: clean drydock_docs
+.PHONY: security
+security:
+ tox -e bandit
+
.PHONY: drydock_docs
drydock_docs:
tox -e docs
.PHONY: clean
clean:
+ rm -rf $(BUILD_DIR)/*
rm -rf build
rm -rf docs/build
rm -rf charts/drydock/charts
diff --git a/drydock_provisioner/drivers/oob/libvirt_driver/__init__.py b/drydock_provisioner/drivers/oob/libvirt_driver/__init__.py
new file mode 100644
index 00000000..f792e17a
--- /dev/null
+++ b/drydock_provisioner/drivers/oob/libvirt_driver/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 AT&T Intellectual Property. All other 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.
diff --git a/drydock_provisioner/drivers/oob/libvirt_driver/actions/__init__.py b/drydock_provisioner/drivers/oob/libvirt_driver/actions/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/drydock_provisioner/drivers/oob/libvirt_driver/actions/oob.py b/drydock_provisioner/drivers/oob/libvirt_driver/actions/oob.py
new file mode 100644
index 00000000..2abccec7
--- /dev/null
+++ b/drydock_provisioner/drivers/oob/libvirt_driver/actions/oob.py
@@ -0,0 +1,377 @@
+# Copyright 2017 AT&T Intellectual Property. All other 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.
+"""Driver for controlling OOB interface via libvirt api."""
+
+import time
+import libvirt
+from urllib.parse import urlparse
+
+import defusedxml.ElementTree as ET
+
+from drydock_provisioner.orchestrator.actions.orchestrator import BaseAction
+
+import drydock_provisioner.error as errors
+
+import drydock_provisioner.objects.fields as hd_fields
+
+
+class LibvirtBaseAction(BaseAction):
+ """Base action for Pyghmi executed actions."""
+
+ def init_session(self, node):
+ """Initialize a Libvirt session to the node hypervisor.
+
+ :param node: instance of objects.BaremetalNode
+ """
+ if node.oob_type != 'libvirt':
+ raise errors.DriverError(
+ "Node OOB type %s is not 'libvirt'" % node.oob_type)
+
+ virsh_url = node.oob_parameters.get('libvirt_uri', None)
+
+ if not virsh_url:
+ raise errors.DriverError("Node %s has no 'libvirt_url' defined" %
+ (node.name))
+
+ url_parts = urlparse(virsh_url)
+
+ if url_parts.scheme != "qemu+ssh":
+ raise errors.DriverError(
+ "Node %s has invalid libvirt URL scheme %s. "
+ "Only 'qemu+ssh' supported." % (node.name, url_parts.scheme))
+
+ self.logger.debug("Starting libvirt session to hypervisor %s " %
+ (virsh_url))
+ virsh_ses = libvirt.open(virsh_url)
+
+ if not virsh_ses:
+ raise errors.DriverError(
+ "Unable to establish libvirt session to %s." % virsh_url)
+
+ return virsh_ses
+
+ def set_node_pxe(self, node):
+ """Set a node to PXE boot first."""
+ ses = self.init_session(node)
+ domain = ses.lookupByName(node.name)
+ domain_xml = domain.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE
+ | libvirt.VIR_DOMAIN_XML_INACTIVE)
+ xmltree = ET.fromstring(domain_xml)
+
+ # Delete all the current boot entries
+ os_tree = xmltree.find("./os")
+ boot_elements = os_tree.findall("./boot")
+ for e in boot_elements:
+ os_tree.remove(e)
+
+ # Now apply our boot order which is 'network' and then 'hd'
+ os_tree.append(ET.fromstring(""))
+ os_tree.append(ET.fromstring(""))
+
+ # And now save the new XML def to the hypervisor
+ domain_xml = ET.tostring(xmltree, encoding="utf-8")
+ ses.defineXML(domain_xml.decode('utf-8'))
+ ses.close()
+
+ def get_node_status(self, node):
+ """Get node status via libvirt api."""
+ try:
+ ses = self.init_session(node)
+ domain = ses.lookupByName(node.name)
+ status = domain.isActive()
+ finally:
+ ses.close()
+
+ return status
+
+ def poweroff_node(self, node):
+ """Power off a node."""
+ ses = self.init_session(node)
+ domain = ses.lookupByName(node.name)
+
+ if domain.isActive():
+ domain.destroy()
+ else:
+ self.logger.debug("Node already powered off.")
+ return
+
+ try:
+ i = 3
+ while i > 0:
+ self.logger.debug("Polling powerstate waiting for success.")
+ power_state = domain.isActive()
+ if not power_state:
+ return
+ time.sleep(10)
+ i = i - 1
+ raise errors.DriverError("Power state never matched off")
+ finally:
+ ses.close()
+
+ def poweron_node(self, node):
+ """Power on a node."""
+ ses = self.init_session(node)
+ domain = ses.lookupByName(node.name)
+
+ if not domain.isActive():
+ domain.create()
+ else:
+ self.logger.debug("Node already powered on.")
+ return
+
+ try:
+ i = 3
+ while i > 0:
+ self.logger.debug("Polling powerstate waiting for success.")
+ power_state = domain.isActive()
+ if power_state:
+ return
+ time.sleep(10)
+ i = i - 1
+ raise errors.DriverError("Power state never matched on")
+ finally:
+ ses.close()
+
+
+class ValidateOobServices(LibvirtBaseAction):
+ """Action to validation OOB services are available."""
+
+ def start(self):
+ self.task.add_status_msg(
+ msg="OOB does not require services.",
+ error=False,
+ ctx='NA',
+ ctx_type='NA')
+ self.task.set_status(hd_fields.TaskStatus.Complete)
+ self.task.success()
+ self.task.save()
+
+ return
+
+
+class ConfigNodePxe(LibvirtBaseAction):
+ """Action to configure PXE booting via OOB."""
+
+ def start(self):
+ self.task.set_status(hd_fields.TaskStatus.Running)
+ self.task.save()
+
+ design_status, site_design = self.orchestrator.get_effective_site(
+ self.task.design_ref)
+ node_list = self.orchestrator.process_node_filter(
+ self.task.node_filter, site_design)
+
+ for n in node_list:
+ self.task.add_status_msg(
+ msg="Libvirt doesn't configure PXE options.",
+ error=True,
+ ctx=n.name,
+ ctx_type='node')
+ self.task.set_status(hd_fields.TaskStatus.Complete)
+ self.task.failure()
+ self.task.save()
+ return
+
+
+class SetNodeBoot(LibvirtBaseAction):
+ """Action to configure a node to PXE boot."""
+
+ def start(self):
+ self.task.set_status(hd_fields.TaskStatus.Running)
+ self.task.save()
+
+ design_status, site_design = self.orchestrator.get_effective_site(
+ self.task.design_ref)
+ node_list = self.orchestrator.process_node_filter(
+ self.task.node_filter, site_design)
+
+ for n in node_list:
+ self.logger.debug("Setting bootdev to PXE for %s" % n.name)
+ self.task.add_status_msg(
+ msg="Setting node to PXE boot.",
+ error=False,
+ ctx=n.name,
+ ctx_type='node')
+
+ try:
+ self.set_node_pxe(n)
+ except Exception as ex:
+ self.task.add_status_msg(
+ msg="Unable to set bootdev to PXE: %s" % str(ex),
+ error=True,
+ ctx=n.name,
+ ctx_type='node')
+ self.task.failure(focus=n.name)
+ self.logger.warning("Unable to set node %s to PXE boot." %
+ (n.name))
+ else:
+ self.task.add_status_msg(
+ msg="Set bootdev to PXE.",
+ error=False,
+ ctx=n.name,
+ ctx_type='node')
+ self.logger.debug("%s reports bootdev of network" % n.name)
+ self.task.success(focus=n.name)
+
+ self.task.set_status(hd_fields.TaskStatus.Complete)
+ self.task.save()
+ return
+
+
+class PowerOffNode(LibvirtBaseAction):
+ """Action to power off a node via libvirt API."""
+
+ def start(self):
+ self.task.set_status(hd_fields.TaskStatus.Running)
+ self.task.save()
+
+ design_status, site_design = self.orchestrator.get_effective_site(
+ self.task.design_ref)
+ node_list = self.orchestrator.process_node_filter(
+ self.task.node_filter, site_design)
+
+ for n in node_list:
+ msg = "Shutting down domain %s" % n.name
+ self.logger.debug(msg)
+ self.task.add_status_msg(
+ msg=msg, error=False, ctx=n.name, ctx_type='node')
+
+ try:
+ self.poweroff_node(n)
+ except Exception as ex:
+ msg = "Node failed to power off: %s" % str(ex)
+ self.task.add_status_msg(
+ msg=msg, error=True, ctx=n.name, ctx_type='node')
+ self.logger.error(msg)
+ self.task.failure(focus=n.name)
+ else:
+ msg = "Node %s powered off." % n.name
+ self.task.add_status_msg(
+ msg=msg, error=False, ctx=n.name, ctx_type='node')
+ self.logger.debug(msg)
+ self.task.success(focus=n.name)
+
+ self.task.set_status(hd_fields.TaskStatus.Complete)
+ self.task.save()
+ return
+
+
+class PowerOnNode(LibvirtBaseAction):
+ """Action to power on a node via libvirt API."""
+
+ def start(self):
+ self.task.set_status(hd_fields.TaskStatus.Running)
+ self.task.save()
+
+ design_status, site_design = self.orchestrator.get_effective_site(
+ self.task.design_ref)
+ node_list = self.orchestrator.process_node_filter(
+ self.task.node_filter, site_design)
+
+ for n in node_list:
+ msg = "Starting domain %s" % n.name
+ self.logger.debug(msg)
+ self.task.add_status_msg(
+ msg=msg, error=False, ctx=n.name, ctx_type='node')
+
+ try:
+ self.poweron_node(n)
+ except Exception as ex:
+ msg = "Node failed to power on: %s" % str(ex)
+ self.task.add_status_msg(
+ msg=msg, error=True, ctx=n.name, ctx_type='node')
+ self.logger.error(msg)
+ self.task.failure(focus=n.name)
+ else:
+ msg = "Node %s powered on." % n.name
+ self.task.add_status_msg(
+ msg=msg, error=False, ctx=n.name, ctx_type='node')
+ self.logger.debug(msg)
+ self.task.success(focus=n.name)
+
+ self.task.set_status(hd_fields.TaskStatus.Complete)
+ self.task.save()
+ return
+
+
+class PowerCycleNode(LibvirtBaseAction):
+ """Action to hard powercycle a node via IPMI."""
+
+ def start(self):
+ self.task.set_status(hd_fields.TaskStatus.Running)
+ self.task.save()
+
+ design_status, site_design = self.orchestrator.get_effective_site(
+ self.task.design_ref)
+ node_list = self.orchestrator.process_node_filter(
+ self.task.node_filter, site_design)
+
+ for n in node_list:
+ msg = ("Power cycling domain for node %s" % n.name)
+ self.logger.debug(msg)
+ self.task.add_status_msg(
+ msg=msg, error=False, ctx=n.name, ctx_type='node')
+
+ try:
+ self.poweroff_node(n)
+ self.poweron_node(n)
+ except Exception as ex:
+ msg = "Node failed to power cycle: %s" % str(ex)
+ self.task.add_status_msg(
+ msg=msg, error=True, ctx=n.name, ctx_type='node')
+ self.logger.error(msg)
+ self.task.failure(focus=n.name)
+ else:
+ msg = "Node %s power cycled." % n.name
+ self.task.add_status_msg(
+ msg=msg, error=False, ctx=n.name, ctx_type='node')
+ self.logger.debug(msg)
+ self.task.success(focus=n.name)
+
+ self.task.set_status(hd_fields.TaskStatus.Complete)
+ self.task.save()
+ return
+
+
+class InterrogateOob(LibvirtBaseAction):
+ """Action to complete a basic interrogation of the node IPMI interface."""
+
+ def start(self):
+ self.task.set_status(hd_fields.TaskStatus.Running)
+ self.task.save()
+
+ design_status, site_design = self.orchestrator.get_effective_site(
+ self.task.design_ref)
+ node_list = self.orchestrator.process_node_filter(
+ self.task.node_filter, site_design)
+
+ for n in node_list:
+ try:
+ node_status = self.get_node_status(n)
+ except Exception as ex:
+ msg = "Node failed tatus check: %s" % str(ex)
+ self.task.add_status_msg(
+ msg=msg, error=True, ctx=n.name, ctx_type='node')
+ self.logger.error(msg)
+ self.task.failure(focus=n.name)
+ else:
+ msg = "Node %s status is %s." % (n.name, node_status)
+ self.task.add_status_msg(
+ msg=msg, error=False, ctx=n.name, ctx_type='node')
+ self.logger.debug(msg)
+ self.task.success(focus=n.name)
+
+ self.task.set_status(hd_fields.TaskStatus.Complete)
+ self.task.save()
+ return
diff --git a/drydock_provisioner/drivers/oob/libvirt_driver/driver.py b/drydock_provisioner/drivers/oob/libvirt_driver/driver.py
new file mode 100644
index 00000000..afec5d55
--- /dev/null
+++ b/drydock_provisioner/drivers/oob/libvirt_driver/driver.py
@@ -0,0 +1,156 @@
+# Copyright 2018 AT&T Intellectual Property. All other 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.
+"""Driver for controlling libvirt domains."""
+
+import uuid
+import logging
+import concurrent.futures
+
+from oslo_config import cfg
+
+import drydock_provisioner.error as errors
+import drydock_provisioner.config as config
+
+import drydock_provisioner.objects.fields as hd_fields
+
+import drydock_provisioner.drivers.oob.driver as oob_driver
+import drydock_provisioner.drivers.driver as generic_driver
+
+from .actions.oob import ValidateOobServices
+from .actions.oob import ConfigNodePxe
+from .actions.oob import SetNodeBoot
+from .actions.oob import PowerOffNode
+from .actions.oob import PowerOnNode
+from .actions.oob import PowerCycleNode
+from .actions.oob import InterrogateOob
+
+
+class LibvirtDriver(oob_driver.OobDriver):
+ """Driver for executing OOB actions via libvirt API."""
+
+ libvirt_driver_options = [
+ cfg.IntOpt(
+ 'poll_interval',
+ default=10,
+ help='Polling interval in seconds for querying libvirt status'),
+ ]
+
+ oob_types_supported = ['libvirt']
+
+ driver_name = "libvirt_driver"
+ driver_key = "libvirt_driver"
+ driver_desc = "Libvirt OOB Driver"
+
+ action_class_map = {
+ hd_fields.OrchestratorAction.ValidateOobServices: ValidateOobServices,
+ hd_fields.OrchestratorAction.ConfigNodePxe: ConfigNodePxe,
+ hd_fields.OrchestratorAction.SetNodeBoot: SetNodeBoot,
+ hd_fields.OrchestratorAction.PowerOffNode: PowerOffNode,
+ hd_fields.OrchestratorAction.PowerOnNode: PowerOnNode,
+ hd_fields.OrchestratorAction.PowerCycleNode: PowerCycleNode,
+ hd_fields.OrchestratorAction.InterrogateOob: InterrogateOob,
+ }
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ cfg.CONF.register_opts(
+ LibvirtDriver.libvirt_driver_options,
+ group=LibvirtDriver.driver_key)
+
+ self.logger = logging.getLogger(
+ config.config_mgr.conf.logging.oobdriver_logger_name)
+
+ def execute_task(self, task_id):
+ task = self.state_manager.get_task(task_id)
+
+ if task is None:
+ self.logger.error("Invalid task %s" % (task_id))
+ raise errors.DriverError("Invalid task %s" % (task_id))
+
+ if task.action not in self.supported_actions:
+ self.logger.error("Driver %s doesn't support task action %s" %
+ (self.driver_desc, task.action))
+ raise errors.DriverError(
+ "Driver %s doesn't support task action %s" % (self.driver_desc,
+ task.action))
+
+ task.set_status(hd_fields.TaskStatus.Running)
+ task.save()
+
+ target_nodes = self.orchestrator.get_target_nodes(task)
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=16) as e:
+ subtask_futures = dict()
+ for n in target_nodes:
+ sub_nf = self.orchestrator.create_nodefilter_from_nodelist([n])
+ subtask = self.orchestrator.create_task(
+ action=task.action,
+ design_ref=task.design_ref,
+ node_filter=sub_nf)
+ task.register_subtask(subtask)
+ self.logger.debug(
+ "Starting Libvirt subtask %s for action %s on node %s" %
+ (str(subtask.get_id()), task.action, n.name))
+
+ action_class = self.action_class_map.get(task.action, None)
+ if action_class is None:
+ self.logger.error(
+ "Could not find action resource for action %s" %
+ task.action)
+ self.task.failure()
+ break
+ action = action_class(subtask, self.orchestrator,
+ self.state_manager)
+ subtask_futures[subtask.get_id().bytes] = e.submit(
+ action.start)
+
+ timeout = config.config_mgr.conf.timeouts.drydock_timeout
+ finished, running = concurrent.futures.wait(
+ subtask_futures.values(), timeout=(timeout * 60))
+
+ for t, f in subtask_futures.items():
+ if not f.done():
+ task.add_status_msg(
+ "Subtask %s timed out before completing.",
+ error=True,
+ ctx=str(uuid.UUID(bytes=t)),
+ ctx_type='task')
+ task.failure()
+ else:
+ if f.exception():
+ self.logger.error(
+ "Uncaught exception in subtask %s" % str(
+ uuid.UUID(bytes=t)),
+ exc_info=f.exception())
+ task.align_result()
+ task.bubble_results()
+ task.set_status(hd_fields.TaskStatus.Complete)
+ task.save()
+
+ return
+
+
+class LibvirtActionRunner(generic_driver.DriverActionRunner):
+ """Threaded runner for a Libvirt Action."""
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ self.logger = logging.getLogger(
+ config.config_mgr.conf.logging.oobdriver_logger_name)
+
+
+def list_opts():
+ return {LibvirtDriver.driver_key: LibvirtDriver.libvirt_driver_options}
diff --git a/hostdeps.sh b/hostdeps.sh
new file mode 100755
index 00000000..8561d030
--- /dev/null
+++ b/hostdeps.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# Install host-level package dependencies
+# needed for local testing
+if [[ ! -z $(uname -a | grep Ubuntu) ]]
+then
+ apt install -y --no-install-recommends $(grep -v '^#' requirements-host.txt)
+else
+ echo "Only support testing on Ubuntu hosts at this time."
+fi
diff --git a/images/drydock/Dockerfile b/images/drydock/Dockerfile
index 9508a5a9..5abf3a70 100644
--- a/images/drydock/Dockerfile
+++ b/images/drydock/Dockerfile
@@ -13,13 +13,15 @@
# limitations under the License.
FROM python:3.5
-ENV DEBIAN_FRONTEND noninteractive
ENV container docker
ENV PORT 9000
ENV LC_ALL C.UTF-8
ENV LANG C.UTF-8
# Copy direct dependency requirements only to build a dependency layer
+RUN DEBIAN_FRONTEND=noninteractive apt update && \
+ apt install -y libvirt-dev --no-install-recommends
+
COPY ./requirements-lock.txt /tmp/drydock/
RUN pip3 install \
--no-cache-dir \
diff --git a/requirements-direct.txt b/requirements-direct.txt
index 814eeab3..3a422a7f 100644
--- a/requirements-direct.txt
+++ b/requirements-direct.txt
@@ -22,3 +22,4 @@ jsonschema==2.6.0
jinja2==2.9.6
ulid2==0.1.1
defusedxml===0.5.0
+libvirt-python==3.10.0
diff --git a/requirements-host.txt b/requirements-host.txt
new file mode 100644
index 00000000..dde8818a
--- /dev/null
+++ b/requirements-host.txt
@@ -0,0 +1,8 @@
+# These are host packages needed for Drydock
+# that don't come on a minimal Ubuntu install
+libvirt-dev
+pkg-config
+python3-dev
+python-tox
+docker.io
+gcc
diff --git a/requirements-lock.txt b/requirements-lock.txt
index f6227eb0..2b81c1c8 100644
--- a/requirements-lock.txt
+++ b/requirements-lock.txt
@@ -20,6 +20,7 @@ jsonschema==2.6.0
keystoneauth1==2.13.0
keystonemiddleware==4.9.1
kombu==4.1.0
+libvirt-python==3.10.0
Mako==1.0.7
MarkupSafe==1.0
monotonic==1.5
diff --git a/tests/unit/test_libvirt_driver.py b/tests/unit/test_libvirt_driver.py
new file mode 100644
index 00000000..95d4f673
--- /dev/null
+++ b/tests/unit/test_libvirt_driver.py
@@ -0,0 +1,69 @@
+# Copyright 2018 AT&T Intellectual Property. All other 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 logging
+
+import libvirt
+import pytest
+
+from drydock_provisioner.error import DriverError
+from drydock_provisioner.drivers.oob.libvirt_driver.actions.oob import LibvirtBaseAction
+
+LOG = logging.getLogger(__name__)
+
+
+class TestLibvirtOobDriver():
+ def test_libvirt_init_session(self, mocker, deckhand_orchestrator,
+ input_files, setup):
+ """Test session initialization."""
+ mocker.patch('libvirt.open')
+ input_file = input_files.join("deckhand_fullsite_libvirt.yaml")
+
+ design_ref = "file://%s" % str(input_file)
+
+ design_status, design_data = deckhand_orchestrator.get_effective_site(
+ design_ref)
+
+ action = LibvirtBaseAction(None, None, None)
+
+ # controller01 should have valid libvirt OOB description
+ node = design_data.get_baremetal_node('controller01')
+
+ LOG.debug("%s", str(node.obj_to_simple()))
+
+ action.init_session(node)
+
+ expected_calls = [mocker.call('qemu+ssh://dummy@somehost/system')]
+
+ libvirt.open.assert_has_calls(expected_calls)
+
+ def test_libvirt_invalid_uri(self, mocker, deckhand_orchestrator,
+ input_files, setup):
+ """Test session initialization."""
+ mocker.patch('libvirt.open')
+ input_file = input_files.join("deckhand_fullsite_libvirt.yaml")
+
+ design_ref = "file://%s" % str(input_file)
+
+ design_status, design_data = deckhand_orchestrator.get_effective_site(
+ design_ref)
+
+ action = LibvirtBaseAction(None, None, None)
+
+ # compute01 should have invalid libvirt OOB description
+ node = design_data.get_baremetal_node('compute01')
+
+ LOG.debug("%s", str(node.obj_to_simple()))
+
+ with pytest.raises(DriverError):
+ action.init_session(node)
diff --git a/tests/yaml_samples/deckhand_fullsite_libvirt.yaml b/tests/yaml_samples/deckhand_fullsite_libvirt.yaml
new file mode 100644
index 00000000..f3ab60a5
--- /dev/null
+++ b/tests/yaml_samples/deckhand_fullsite_libvirt.yaml
@@ -0,0 +1,424 @@
+#Copyright 2017 AT&T Intellectual Property. All other 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.
+####################
+#
+# deckhand_fullsite_libvirt.yaml - Full site definition in Deckhand format for libvirt based nodes
+#
+####################
+---
+schema: 'drydock/Region/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: 'sitename'
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ tag_definitions:
+ - tag: 'test'
+ definition_type: 'lshw_xpath'
+ definition: "//node[@id=\"display\"]/'clock units=\"Hz\"' > 1000000000"
+ authorized_keys:
+ - |
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDENeyO5hLPbLLQRZ0oafTYWs1ieo5Q+XgyZQs51Ju
+ jDGc8lKlWsg1/6yei2JewKMgcwG2Buu1eqU92Xn1SvMZLyt9GZURuBkyjcfVc/8GiU5QP1Of8B7CV0c
+ kfUpHWYJ17olTzT61Hgz10ioicBF6cjgQrLNcyn05xoaJHD2Vpf8Unxzi0YzA2e77yRqBo9jJVRaX2q
+ wUJuZrzb62x3zw8Knz6GGSZBn8xRKLaw1SKFpd1hwvL62GfqX5ZBAT1AYTZP1j8GcAoK8AFVn193SEU
+ vjSdUFa+RNWuJhkjBRfylJczIjTIFb5ls0jpbA3bMA9DE7lFKVQl6vVwFmiIVBI1 samplekey
+---
+schema: 'drydock/NetworkLink/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: pxe
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ bonding:
+ mode: disabled
+ mtu: 1500
+ linkspeed: auto
+ trunking:
+ mode: disabled
+ default_network: pxe
+ allowed_networks:
+ - pxe
+---
+schema: 'drydock/NetworkLink/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: gp
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ bonding:
+ mode: 802.3ad
+ hash: layer3+4
+ peer_rate: slow
+ mtu: 9000
+ linkspeed: auto
+ trunking:
+ mode: 802.1q
+ default_network: mgmt
+ allowed_networks:
+ - public
+ - private
+ - mgmt
+---
+schema: 'drydock/Rack/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: rack1
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ tor_switches:
+ switch01name:
+ mgmt_ip: 1.1.1.1
+ sdn_api_uri: polo+https://polo-api.web.att.com/switchmgmt?switch=switch01name
+ switch02name:
+ mgmt_ip: 1.1.1.2
+ sdn_api_uri: polo+https://polo-api.web.att.com/switchmgmt?switch=switch02name
+ location:
+ clli: HSTNTXMOCG0
+ grid: EG12
+ local_networks:
+ - pxe-rack1
+---
+schema: 'drydock/Network/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: pxe
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ dhcp_relay:
+ self_ip: 172.16.0.4
+ upstream_target: 172.16.5.5
+ mtu: 1500
+ cidr: 172.16.0.0/24
+ ranges:
+ - type: dhcp
+ start: 172.16.0.5
+ end: 172.16.0.254
+ dns:
+ domain: admin.sitename.att.com
+ servers: 172.16.0.10
+---
+schema: 'drydock/Network/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: mgmt
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ vlan: '100'
+ mtu: 1500
+ cidr: 172.16.1.0/24
+ ranges:
+ - type: static
+ start: 172.16.1.15
+ end: 172.16.1.254
+ routes:
+ - subnet: 0.0.0.0/0
+ gateway: 172.16.1.1
+ metric: 10
+ dns:
+ domain: mgmt.sitename.example.com
+ servers: 172.16.1.9,172.16.1.10
+---
+schema: 'drydock/Network/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: private
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ vlan: '101'
+ mtu: 9000
+ cidr: 172.16.2.0/24
+ ranges:
+ - type: static
+ start: 172.16.2.15
+ end: 172.16.2.254
+ dns:
+ domain: priv.sitename.example.com
+ servers: 172.16.2.9,172.16.2.10
+---
+schema: 'drydock/Network/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: public
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ vlan: '102'
+ mtu: 1500
+ cidr: 172.16.3.0/24
+ ranges:
+ - type: static
+ start: 172.16.3.15
+ end: 172.16.3.254
+ routes:
+ - subnet: 0.0.0.0/0
+ gateway: 172.16.3.1
+ metric: 10
+ dns:
+ domain: sitename.example.com
+ servers: 8.8.8.8
+---
+schema: 'drydock/HostProfile/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: defaults
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ oob:
+ type: libvirt
+ libvirt_uri: qemu+ssh://dummy@somehost/system
+ storage:
+ physical_devices:
+ sda:
+ labels:
+ role: rootdisk
+ partitions:
+ - name: root
+ size: 20g
+ bootable: true
+ filesystem:
+ mountpoint: '/'
+ fstype: 'ext4'
+ mount_options: 'defaults'
+ - name: boot
+ size: 1g
+ bootable: false
+ filesystem:
+ mountpoint: '/boot'
+ fstype: 'ext4'
+ mount_options: 'defaults'
+ sdb:
+ volume_group: 'log_vg'
+ volume_groups:
+ log_vg:
+ logical_volumes:
+ - name: 'log_lv'
+ size: '500m'
+ filesystem:
+ mountpoint: '/var/log'
+ fstype: 'xfs'
+ mount_options: 'defaults'
+ hardware_profile: HPGen9v3
+ primary_network: mgmt
+ interfaces:
+ pxe:
+ device_link: pxe
+ labels:
+ noconfig: true
+ slaves:
+ - prim_nic01
+ networks:
+ - pxe
+ bond0:
+ device_link: gp
+ slaves:
+ - prim_nic01
+ - prim_nic02
+ networks:
+ - mgmt
+ - private
+ sriov:
+ vf_count: 2
+ trustedmode: false
+ platform:
+ image: 'xenial'
+ kernel: 'ga-16.04'
+ kernel_params:
+ quiet: true
+ console: ttyS2
+ metadata:
+ owner_data:
+ foo: bar
+---
+schema: 'drydock/BaremetalNode/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: controller01
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ host_profile: defaults
+ addressing:
+ - network: pxe
+ address: dhcp
+ - network: mgmt
+ address: 172.16.1.20
+ - network: public
+ address: 172.16.3.20
+ metadata:
+ rack: rack1
+---
+schema: 'drydock/BaremetalNode/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: compute01
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ host_profile: defaults
+ oob:
+ type: libvirt
+# This is an invalid libvirt uri, use it for testing
+# sanity checks
+ libvirt_uri: http://dummy@somehost/system
+ addressing:
+ - network: pxe
+ address: dhcp
+ - network: mgmt
+ address: 172.16.1.21
+ - network: private
+ address: 172.16.2.21
+ - network: oob
+ address: 172.16.100.21
+ platform:
+ kernel_params:
+ isolcpus: hardwareprofile:cpuset.sriov
+ hugepagesz: hardwareprofile:hugepages.sriov.size
+ hugepages: hardwareprofile:hugepages.sriov.count
+ metadata:
+ rack: rack2
+---
+schema: 'drydock/HardwareProfile/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: HPGen9v3
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ vendor: HP
+ generation: '8'
+ hw_version: '3'
+ bios_version: '2.2.3'
+ boot_mode: bios
+ bootstrap_protocol: pxe
+ pxe_interface: 0
+ device_aliases:
+ prim_nic01:
+ address: '0000:00:03.0'
+ dev_type: '82540EM Gigabit Ethernet Controller'
+ bus_type: 'pci'
+ prim_nic02:
+ address: '0000:00:04.0'
+ dev_type: '82540EM Gigabit Ethernet Controller'
+ bus_type: 'pci'
+ primary_boot:
+ address: '2:0.0.0'
+ dev_type: 'VBOX HARDDISK'
+ bus_type: 'scsi'
+ cpu_sets:
+ sriov: '2,4'
+ hugepages:
+ sriov:
+ size: '1G'
+ count: 300
+ dpdk:
+ size: '2M'
+ count: 530000
+---
+schema: 'drydock/BootAction/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: hw_filtered
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ signaling: false
+ node_filter:
+ filter_set_type: 'union'
+ filter_set:
+ - filter_type: 'union'
+ node_names:
+ - 'compute01'
+ assets:
+ - path: /var/tmp/hello.sh
+ type: file
+ permissions: '555'
+ data: |-
+ IyEvYmluL2Jhc2gKCmVjaG8gJ0hlbGxvIFdvcmxkISAtZnJvbSB7eyBub2RlLmhvc3RuYW1lIH19
+ Jwo=
+ data_pipeline:
+ - base64_decode
+ - utf8_decode
+ - template
+ - path: /lib/systemd/system/hello.service
+ type: unit
+ permissions: '600'
+ data: |-
+ W1VuaXRdCkRlc2NyaXB0aW9uPUhlbGxvIFdvcmxkCgpbU2VydmljZV0KVHlwZT1vbmVzaG90CkV4
+ ZWNTdGFydD0vdmFyL3RtcC9oZWxsby5zaAoKW0luc3RhbGxdCldhbnRlZEJ5PW11bHRpLXVzZXIu
+ dGFyZ2V0Cg==
+ data_pipeline:
+ - base64_decode
+ - utf8_decode
+...
+---
+schema: 'drydock/BootAction/v1'
+metadata:
+ schema: 'metadata/Document/v1'
+ name: helloworld
+ storagePolicy: 'cleartext'
+ labels:
+ application: 'drydock'
+data:
+ assets:
+ - path: /var/tmp/hello.sh
+ type: file
+ permissions: '555'
+ data: |-
+ IyEvYmluL2Jhc2gKCmVjaG8gJ0hlbGxvIFdvcmxkISAtZnJvbSB7eyBub2RlLmhvc3RuYW1lIH19
+ Jwo=
+ data_pipeline:
+ - base64_decode
+ - utf8_decode
+ - template
+ - path: /lib/systemd/system/hello.service
+ type: unit
+ permissions: '600'
+ data: |-
+ W1VuaXRdCkRlc2NyaXB0aW9uPUhlbGxvIFdvcmxkCgpbU2VydmljZV0KVHlwZT1vbmVzaG90CkV4
+ ZWNTdGFydD0vdmFyL3RtcC9oZWxsby5zaAoKW0luc3RhbGxdCldhbnRlZEJ5PW11bHRpLXVzZXIu
+ dGFyZ2V0Cg==
+ data_pipeline:
+ - base64_decode
+ - utf8_decode
+ - path: /var/tmp/designref.sh
+ type: file
+ permissions: '500'
+ data: e3sgYWN0aW9uLmRlc2lnbl9yZWYgfX0K
+ data_pipeline:
+ - base64_decode
+ - utf8_decode
+ - template
+...
diff --git a/tools/helm_install.sh b/tools/helm_install.sh
new file mode 100755
index 00000000..15f1cf9b
--- /dev/null
+++ b/tools/helm_install.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+# Copyright 2018 AT&T Intellectual Property. All other 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.
+#
+
+set -x
+
+HELM=$1
+HELM_ARTIFACT_URL=${HELM_ARTIFACT_URL:-"https://storage.googleapis.com/kubernetes-helm/helm-v2.7.2-linux-amd64.tar.gz"}
+
+
+function install_helm_binary {
+ if [[ -z "${HELM}" ]]
+ then
+ echo "No Helm binary target location."
+ exit -1
+ fi
+
+ if [[ -w "$(dirname ${HELM})" ]]
+ then
+ TMP_DIR=${BUILD_DIR:-$(mktemp -d)}
+ curl -o "${TMP_DIR}/helm.tar.gz" "${HELM_ARTIFACT_URL}"
+ cd ${TMP_DIR}
+ tar -xvzf helm.tar.gz
+ cp "${TMP_DIR}/linux-amd64/helm" "${HELM}"
+ else
+ echo "Cannot write to ${HELM}"
+ exit -1
+ fi
+}
+
+install_helm_binary
diff --git a/tools/helm_tk.sh b/tools/helm_tk.sh
index 51c906d8..0b3abdeb 100755
--- a/tools/helm_tk.sh
+++ b/tools/helm_tk.sh
@@ -18,6 +18,7 @@
HELM=$1
HTK_REPO=${HTK_REPO:-"https://github.com/openstack/openstack-helm"}
HTK_PATH=${HTK_PATH:-""}
+HTK_STABLE_COMMIT=${HTK_COMMIT:-"f902cd14fac7de4c4c9f7d019191268a6b4e9601"}
DEP_UP_LIST=${DEP_UP_LIST:-"drydock"}
if [[ ! -z $(echo $http_proxy) ]]
@@ -52,10 +53,10 @@ function helm_serve {
mkdir -p build
pushd build
-git clone --depth 1 $HTK_REPO || true
+git clone $HTK_REPO || true
pushd openstack-helm/$HTK_PATH
+git reset --hard "${HTK_STABLE_COMMIT}"
-git pull
helm_serve
make helm-toolkit
popd && popd