/docs_build/
 /docs_output/
 /releng/
+venv/
 apex.egg-info/
 coverage.xml
 nosetests.xml
 
 CUSTOM_OVS = '{}/apex/random/openvswitch-2.9.0-9.el7fdn.x86_64.' \
              'rpm'.format(OPNFV_ARTIFACTS)
 
-QUAGGA_URL = "http://artifacts.opnfv.org/sdnvpn/quagga/quagga-4.tar.gz"
-
 OVS_URL = "http://openvswitch.org/releases/openvswitch-2.9.2.tar.gz"
+QUAGGA_URL = "{}/sdnvpn/quagga/quagga-4.tar.gz".format(OPNFV_ARTIFACTS)
 
 
 class ApexBuildException(Exception):
     pass
+
+
+class SnapshotDeployException(Exception):
+    pass
+
+
+class OvercloudNodeException(Exception):
+    pass
+
+
+class FetchException(Exception):
+    pass
 
         if download_target:
             urllib.request.urlretrieve(target_url, filename=target_dest)
             logging.info("Target downloaded: {}".format(target))
-        if target.endswith('.tar'):
+        if target.endswith(('.tar', 'tar.gz', 'tgz')):
             logging.info('Unpacking tar file')
             tar = tarfile.open(target_dest)
             tar.extractall(path=dest)
     try:
         response = urllib.request.urlopen(url, timeout=timeout)
         return response.read()
-    except (urllib.request.URLError, socket.timeout):
+    except (urllib.request.URLError, socket.timeout) as e:
         logging.error("Unable to open URL: {}".format(url))
-        raise
+        raise exc.FetchException('Unable to open URL') from e
 
 
 def edit_tht_env(env_file, section, settings):
         if x not in uniq_list:
             uniq_list.append(x)
     return uniq_list
+
+
+def bash_settings_to_dict(data):
+    """
+    Parses bash settings x=y and returns dict of key, values
+    :param data: bash settings data in x=y format
+    :return: dict of keys and values
+    """
+    return dict(item.split('=') for item in data.splitlines())
+
+
+def fetch_properties(url):
+    """
+    Downloads OPNFV properties and returns a dictionary of the key, values
+    :param url: URL of properties file
+    :return: dict of k,v for each properties
+    """
+    if bool(urllib.parse.urlparse(url).scheme):
+        logging.debug('Fetching properties from internet: {}'.format(url))
+        return bash_settings_to_dict(open_webpage(url).decode('utf-8'))
+    elif os.path.isfile(url):
+        logging.debug('Fetching properties from file: {}'.format(url))
+        with open(url, 'r') as fh:
+            data = fh.read()
+        return bash_settings_to_dict(data)
+    else:
+        logging.warning('Unable to fetch properties for: {}'.format(url))
+        raise exc.FetchException('Unable determine properties location: '
+                                 '{}'.format(url))
 
 from apex import Inventory
 from apex import NetworkEnvironment
 from apex import NetworkSettings
+from apex.deployment.snapshot import SnapshotDeployment
 from apex.common import utils
 from apex.common import constants
 from apex.common import parsers
 SDN_IMAGE = 'overcloud-full-opendaylight.qcow2'
 
 
-def deploy_quickstart(args, deploy_settings_file, network_settings_file,
-                      inventory_file=None):
-    pass
-
-
 def validate_cross_settings(deploy_settings, net_settings, inventory):
     """
     Used to validate compatibility across settings file.
                                help='File which contains Apex deploy settings')
     deploy_parser.add_argument('-n', '--network-settings',
                                dest='network_settings_file',
-                               required=True,
+                               required=False,
                                help='File which contains Apex network '
                                     'settings')
     deploy_parser.add_argument('-i', '--inventory-file',
                                default='/usr/share/opnfv-apex',
                                help='Directory path for apex ansible '
                                     'and third party libs')
-    deploy_parser.add_argument('--quickstart', action='store_true',
+    deploy_parser.add_argument('-s', '--snapshot', action='store_true',
                                default=False,
-                               help='Use tripleo-quickstart to deploy')
+                               help='Use snapshots for deployment')
+    deploy_parser.add_argument('--snap-cache', dest='snap_cache',
+                               default="{}/snap_cache".format(
+                                   os.path.expanduser('~')),
+                               help='Local directory to cache snapshot '
+                                    'artifacts. Defaults to $HOME/snap_cache')
     deploy_parser.add_argument('--upstream', action='store_true',
                                default=True,
                                help='Force deployment to use upstream '
     """
 
     logging.debug('Validating arguments for deployment')
-    if args.virtual and args.inventory_file is not None:
+    if args.snapshot:
+        logging.debug('Skipping inventory validation as it is not applicable'
+                      'to snapshot deployments')
+    elif args.virtual and args.inventory_file is not None:
         logging.error("Virtual enabled but inventory file also given")
         raise ApexDeployException('You should not specify an inventory file '
                                   'with virtual deployments')
     elif args.virtual:
         args.inventory_file = os.path.join(APEX_TEMP_DIR,
                                            'inventory-virt.yaml')
-    elif os.path.isfile(args.inventory_file) is False:
+    elif not os.path.isfile(args.inventory_file):
         logging.error("Specified inventory file does not exist: {}".format(
             args.inventory_file))
         raise ApexDeployException('Specified inventory file does not exist')
 
     for settings_file in (args.deploy_settings_file,
                           args.network_settings_file):
+        if settings_file == args.network_settings_file and args.snapshot:
+            continue
         if os.path.isfile(settings_file) is False:
             logging.error("Specified settings file does not "
                           "exist: {}".format(settings_file))
     deploy_settings = DeploySettings(args.deploy_settings_file)
     logging.info("Deploy settings are:\n {}".format(pprint.pformat(
         deploy_settings)))
-    net_settings = NetworkSettings(args.network_settings_file)
-    logging.info("Network settings are:\n {}".format(pprint.pformat(
-        net_settings)))
-    os_version = deploy_settings['deploy_options']['os_version']
-    net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
-    net_env = NetworkEnvironment(net_settings, net_env_file,
-                                 os_version=os_version)
-    net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
-    utils.dump_yaml(dict(net_env), net_env_target)
-
-    # get global deploy params
-    ha_enabled = deploy_settings['global_params']['ha_enabled']
-    introspect = deploy_settings['global_params'].get('introspect', True)
-
-    if args.virtual:
-        if args.virt_compute_ram is None:
-            compute_ram = args.virt_default_ram
-        else:
-            compute_ram = args.virt_compute_ram
-        if deploy_settings['deploy_options']['sdn_controller'] == \
-                'opendaylight' and args.virt_default_ram < 12:
-            control_ram = 12
-            logging.warning('RAM per controller is too low.  OpenDaylight '
-                            'requires at least 12GB per controller.')
-            logging.info('Increasing RAM per controller to 12GB')
-        elif args.virt_default_ram < 10:
-            control_ram = 10
-            logging.warning('RAM per controller is too low.  nosdn '
-                            'requires at least 10GB per controller.')
-            logging.info('Increasing RAM per controller to 10GB')
-        else:
-            control_ram = args.virt_default_ram
-        if ha_enabled and args.virt_compute_nodes < 2:
-            logging.debug('HA enabled, bumping number of compute nodes to 2')
-            args.virt_compute_nodes = 2
-        virt_utils.generate_inventory(args.inventory_file, ha_enabled,
-                                      num_computes=args.virt_compute_nodes,
-                                      controller_ram=control_ram * 1024,
-                                      compute_ram=compute_ram * 1024,
-                                      vcpus=args.virt_cpus
-                                      )
-    inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
-    logging.info("Inventory is:\n {}".format(pprint.pformat(
-        inventory)))
-
-    validate_cross_settings(deploy_settings, net_settings, inventory)
+
+    if not args.snapshot:
+        net_settings = NetworkSettings(args.network_settings_file)
+        logging.info("Network settings are:\n {}".format(pprint.pformat(
+            net_settings)))
+        os_version = deploy_settings['deploy_options']['os_version']
+        net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
+        net_env = NetworkEnvironment(net_settings, net_env_file,
+                                     os_version=os_version)
+        net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
+        utils.dump_yaml(dict(net_env), net_env_target)
+
+        # get global deploy params
+        ha_enabled = deploy_settings['global_params']['ha_enabled']
+        introspect = deploy_settings['global_params'].get('introspect', True)
+        net_list = net_settings.enabled_network_list
+        if args.virtual:
+            if args.virt_compute_ram is None:
+                compute_ram = args.virt_default_ram
+            else:
+                compute_ram = args.virt_compute_ram
+            if (deploy_settings['deploy_options']['sdn_controller'] ==
+                    'opendaylight' and args.virt_default_ram < 12):
+                control_ram = 12
+                logging.warning('RAM per controller is too low.  OpenDaylight '
+                                'requires at least 12GB per controller.')
+                logging.info('Increasing RAM per controller to 12GB')
+            elif args.virt_default_ram < 10:
+                control_ram = 10
+                logging.warning('RAM per controller is too low.  nosdn '
+                                'requires at least 10GB per controller.')
+                logging.info('Increasing RAM per controller to 10GB')
+            else:
+                control_ram = args.virt_default_ram
+            if ha_enabled and args.virt_compute_nodes < 2:
+                logging.debug(
+                    'HA enabled, bumping number of compute nodes to 2')
+                args.virt_compute_nodes = 2
+            virt_utils.generate_inventory(args.inventory_file, ha_enabled,
+                                          num_computes=args.virt_compute_nodes,
+                                          controller_ram=control_ram * 1024,
+                                          compute_ram=compute_ram * 1024,
+                                          vcpus=args.virt_cpus
+                                          )
+        inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
+        logging.info("Inventory is:\n {}".format(pprint.pformat(
+            inventory)))
+
+        validate_cross_settings(deploy_settings, net_settings, inventory)
+    else:
+        # only one network with snapshots
+        net_list = [constants.ADMIN_NETWORK]
+
     ds_opts = deploy_settings['deploy_options']
-    if args.quickstart:
-        deploy_settings_file = os.path.join(APEX_TEMP_DIR,
-                                            'apex_deploy_settings.yaml')
-        utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
-                        deploy_settings_file)
-        logging.info("File created: {}".format(deploy_settings_file))
-        network_settings_file = os.path.join(APEX_TEMP_DIR,
-                                             'apex_network_settings.yaml')
-        utils.dump_yaml(utils.dict_objects_to_str(net_settings),
-                        network_settings_file)
-        logging.info("File created: {}".format(network_settings_file))
-        deploy_quickstart(args, deploy_settings_file, network_settings_file,
-                          args.inventory_file)
+    ansible_args = {
+        'virsh_enabled_networks': net_list,
+        'snapshot': args.snapshot
+    }
+    utils.run_ansible(ansible_args,
+                      os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
+                                   'deploy_dependencies.yml'))
+    if args.snapshot:
+        # Start snapshot Deployment
+        logging.info('Executing Snapshot Deployment...')
+        SnapshotDeployment(deploy_settings=deploy_settings,
+                           snap_cache_dir=args.snap_cache,
+                           fetch=not args.no_fetch,
+                           all_in_one=not bool(args.virt_compute_nodes))
     else:
+        # Start Standard TripleO Deployment
         deployment = ApexDeployment(deploy_settings, args.patches_file,
                                     args.deploy_settings_file)
         # TODO (trozet): add logic back from:
         # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
-        ansible_args = {
-            'virsh_enabled_networks': net_settings.enabled_network_list
-        }
-        utils.run_ansible(ansible_args,
-                          os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
-                                       'deploy_dependencies.yml'))
         uc_external = False
         if 'external' in net_settings.enabled_network_list:
             uc_external = True
 
--- /dev/null
+##############################################################################
+# Copyright (c) 2018 Tim Rozet (trozet@redhat.com) and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+import fnmatch
+import logging
+import os
+import pprint
+import socket
+import time
+
+import libvirt
+
+import apex.common.constants as con
+from apex.common import exceptions as exc
+from apex.common import utils
+from apex.overcloud.node import OvercloudNode
+import apex.settings.deploy_settings as ds
+
+
+SNAP_FILE = 'snapshot.properties'
+CHECKSUM = 'OPNFV_SNAP_SHA512SUM'
+OVERCLOUD_RC = 'overcloudrc'
+SSH_KEY = 'id_rsa'
+OPENSTACK = 'openstack'
+OPENDAYLIGHT = 'opendaylight'
+SERVICES = (OPENSTACK, OPENDAYLIGHT)
+
+
+class SnapshotDeployment:
+    def __init__(self, deploy_settings, snap_cache_dir, fetch=True,
+                 all_in_one=False):
+        self.id_rsa = None
+        self.fetch = fetch
+        ds_opts = deploy_settings['deploy_options']
+        self.os_version = ds_opts['os_version']
+        self.ha_enabled = deploy_settings['global_params']['ha_enabled']
+        if self.ha_enabled:
+            self.ha_ext = 'ha'
+        elif all_in_one:
+            self.ha_ext = 'noha-allinone'
+        else:
+            self.ha_ext = 'noha'
+        self.snap_cache_dir = os.path.join(snap_cache_dir,
+                                           "{}/{}".format(self.os_version,
+                                                          self.ha_ext))
+        self.networks = []
+        self.oc_nodes = []
+        self.properties_url = "{}/apex/{}/{}".format(con.OPNFV_ARTIFACTS,
+                                                     self.os_version,
+                                                     self.ha_ext)
+        self.conn = libvirt.open('qemu:///system')
+        if not self.conn:
+            raise exc.SnapshotDeployException(
+                'Unable to open libvirt connection')
+        if self.fetch:
+            self.pull_snapshot(self.properties_url, self.snap_cache_dir)
+        else:
+            logging.info('No fetch enabled. Will not attempt to pull latest '
+                         'snapshot')
+        self.deploy_snapshot()
+
+    @staticmethod
+    def pull_snapshot(url_path, snap_cache_dir):
+        """
+        Compare opnfv properties file and download and unpack snapshot if
+        necessary
+        :param url_path: path of latest snap info
+        :param snap_cache_dir: local directory for snap cache
+        :return: None
+        """
+        full_url = os.path.join(url_path, SNAP_FILE)
+        upstream_props = utils.fetch_properties(full_url)
+        logging.debug("Upstream properties are: {}".format(upstream_props))
+        try:
+            upstream_sha = upstream_props[CHECKSUM]
+        except KeyError:
+            logging.error('Unable to find {} for upstream properties: '
+                          '{}'.format(CHECKSUM, upstream_props))
+            raise exc.SnapshotDeployException('Unable to find upstream '
+                                              'properties checksum value')
+        local_prop_file = os.path.join(snap_cache_dir, SNAP_FILE)
+        try:
+            local_props = utils.fetch_properties(local_prop_file)
+            local_sha = local_props[CHECKSUM]
+            pull_snap = local_sha != upstream_sha
+        except (exc.FetchException, KeyError):
+            logging.info("No locally cached properties found, will pull "
+                         "latest")
+            local_sha = None
+            pull_snap = True
+        logging.debug('Local sha: {}, Upstream sha: {}'.format(local_sha,
+                                                               upstream_sha))
+        if pull_snap:
+            logging.info('SHA mismatch, will download latest snapshot')
+            full_snap_url = upstream_props['OPNFV_SNAP_URL']
+            snap_file = os.path.basename(full_snap_url)
+            snap_url = full_snap_url.replace(snap_file, '')
+            if not snap_url.startswith('http://'):
+                snap_url = 'http://' + snap_url
+            utils.fetch_upstream_and_unpack(dest=snap_cache_dir,
+                                            url=snap_url,
+                                            targets=[SNAP_FILE, snap_file]
+                                            )
+        else:
+            logging.info('SHA match, artifacts in cache are already latest. '
+                         'Will not download.')
+
+    def create_networks(self):
+        logging.info("Detecting snapshot networks")
+        try:
+            xmls = fnmatch.filter(os.listdir(self.snap_cache_dir), '*.xml')
+        except FileNotFoundError:
+            raise exc.SnapshotDeployException(
+                'No XML files found in snap cache directory: {}'.format(
+                    self.snap_cache_dir))
+        net_xmls = list()
+        for xml in xmls:
+            if xml.startswith('baremetal'):
+                continue
+            net_xmls.append(os.path.join(self.snap_cache_dir, xml))
+        if not net_xmls:
+            raise exc.SnapshotDeployException(
+                'No network XML files detected in snap cache, '
+                'please check local snap cache contents')
+        logging.info('Snapshot networks found: {}'.format(net_xmls))
+        for xml in net_xmls:
+            logging.debug('Creating network from {}'.format(xml))
+            with open(xml, 'r') as fh:
+                net_xml = fh.read()
+            net = self.conn.networkCreateXML(net_xml)
+            self.networks.append(net)
+            logging.info('Network started: {}'.format(net.name()))
+
+    def parse_and_create_nodes(self):
+        """
+        Parse snapshot node.yaml config file and create overcloud nodes
+        :return: None
+        """
+        node_file = os.path.join(self.snap_cache_dir, 'node.yaml')
+        if not os.path.isfile(node_file):
+            raise exc.SnapshotDeployException('Missing node definitions from '
+                                              ''.format(node_file))
+        node_data = utils.parse_yaml(node_file)
+        if 'servers' not in node_data:
+            raise exc.SnapshotDeployException('Invalid node.yaml format')
+        for node, data in node_data['servers'].items():
+            logging.info('Creating node: {}'.format(node))
+            logging.debug('Node data is:\n{}'.format(pprint.pformat(data)))
+            node_xml = os.path.join(self.snap_cache_dir,
+                                    '{}.xml'.format(data['vNode-name']))
+            node_qcow = os.path.join(self.snap_cache_dir,
+                                     '{}.qcow2'.format(data['vNode-name']))
+            self.oc_nodes.append(
+                OvercloudNode(ip=data['address'],
+                              ovs_ctrlrs=data['ovs-controller'],
+                              ovs_mgrs=data['ovs-managers'],
+                              role=data['type'],
+                              name=node,
+                              node_xml=node_xml,
+                              disk_img=node_qcow)
+            )
+            logging.info('Node Created')
+        logging.info('Starting nodes')
+        for node in self.oc_nodes:
+            node.start()
+
+    def get_controllers(self):
+        controllers = []
+        for node in self.oc_nodes:
+            if node.role == 'controller':
+                controllers.append(node)
+        return controllers
+
+    def is_service_up(self, service):
+        assert service in SERVICES
+        if service == OPENSTACK:
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            sock.settimeout(5)
+        controllers = self.get_controllers()
+        if not controllers:
+            raise exc.SnapshotDeployException('No OpenStack controllers found')
+
+        for node in controllers:
+            logging.info('Waiting until {} is up on controller: '
+                         '{}'.format(service, node.name))
+            for x in range(10):
+                logging.debug('Checking {} is up attempt {}'.format(service,
+                              str(x + 1)))
+                if service == OPENSTACK:
+                    # Check if Neutron is up
+                    if sock.connect_ex((node.ip, 9696)) == 0:
+                        logging.info('{} is up on controller {}'.format(
+                                     service, node.name))
+                        break
+                elif service == OPENDAYLIGHT:
+                    url = 'http://{}:8081/diagstatus'.format(node.ip)
+                    try:
+                        utils.open_webpage(url)
+                        logging.info('{} is up on controller {}'.format(
+                                     service, node.name))
+                        break
+                    except Exception as e:
+                        logging.debug('Cannot contact ODL. Reason: '
+                                      '{}'.format(e))
+                time.sleep(60)
+            else:
+                logging.error('{} is not running after 10 attempts'.format(
+                    service))
+                return False
+        return True
+
+    def deploy_snapshot(self):
+        # bring up networks
+        self.create_networks()
+        # check overcloudrc exists, id_rsa
+        for snap_file in (OVERCLOUD_RC, SSH_KEY):
+            if not os.path.isfile(os.path.join(self.snap_cache_dir,
+                                               snap_file)):
+                logging.warning('File is missing form snap cache: '
+                                '{}'.format(snap_file))
+        # create nodes
+        self.parse_and_create_nodes()
+        # validate deployment
+        if self.is_service_up(OPENSTACK):
+            logging.info('OpenStack is up')
+        else:
+            raise exc.SnapshotDeployException('OpenStack is not alive')
+        if self.is_service_up(OPENDAYLIGHT):
+            logging.info('OpenDaylight is up')
+        else:
+            raise exc.SnapshotDeployException(
+                'OpenDaylight {} is not reporting diag status')
+        # TODO(trozet): recreate external network/subnet if missing
+        logging.info('Snapshot deployment complete. Please use the {} file '
+                     'in {} to interact with '
+                     'OpenStack'.format(OVERCLOUD_RC, self.snap_cache_dir))
 
--- /dev/null
+##############################################################################
+# Copyright (c) 2018 Tim Rozet (trozet@redhat.com) and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import logging
+import os
+import shutil
+import xml.etree.ElementTree as ET
+
+import distro
+import libvirt
+
+from apex.common.exceptions import OvercloudNodeException
+
+
+class OvercloudNode:
+    """
+    Overcloud server
+    """
+    def __init__(self, role, ip, ovs_ctrlrs, ovs_mgrs, name, node_xml,
+                 disk_img):
+        self.role = role
+        self.ip = ip
+        self.ovs_ctrlrs = ovs_ctrlrs
+        self.ovs_mgrs = ovs_mgrs
+        self.name = name
+        self.node_xml_file = node_xml
+        self.node_xml = None
+        self.vm = None
+        self.disk_img = None
+        if not os.path.isfile(self.node_xml_file):
+            raise OvercloudNodeException('XML definition file not found: '
+                                         '{}'.format(self.node_xml_file))
+        if not os.path.isfile(disk_img):
+            raise OvercloudNodeException('Disk image file not found: '
+                                         '{}'.format(disk_img))
+        self.conn = libvirt.open('qemu:///system')
+        if not self.conn:
+            raise OvercloudNodeException('Unable to open libvirt connection')
+
+        self.create(src_disk=disk_img)
+
+    def _configure_disk(self, disk):
+        # find default storage pool path
+        pool = self.conn.storagePoolLookupByName('default')
+        if pool is None:
+            raise OvercloudNodeException('Cannot find default storage pool')
+        pool_xml = pool.XMLDesc()
+        logging.debug('Default storage pool xml: {}'.format(pool_xml))
+        etree = ET.fromstring(pool_xml)
+        try:
+            path = etree.find('target').find('path').text
+            logging.info('System libvirt default pool path: {}'.format(path))
+        except AttributeError as e:
+            logging.error('Failure to find libvirt storage path: {}'.format(
+                e))
+            raise OvercloudNodeException('Cannot find default storage path')
+        # copy disk to system path
+        self.disk_img = os.path.join(path, os.path.basename(disk))
+        logging.info('Copying disk image to: {}. This may take some '
+                     'time...'.format(self.disk_img))
+        shutil.copyfile(disk, self.disk_img)
+
+    @staticmethod
+    def _update_xml(xml, disk_path=None):
+        """
+        Updates a libvirt XML file for the current architecture and OS of this
+        machine
+        :param xml: XML string of Libvirt domain definition
+        :param disk_path: Optional file path to update for the backing disk
+        image
+        :return: Updated XML
+        """
+        logging.debug('Parsing xml')
+        try:
+            etree = ET.fromstring(xml)
+        except ET.ParseError:
+            logging.error('Unable to parse node XML: {}'.format(xml))
+            raise OvercloudNodeException('Unable to parse node XML')
+
+        try:
+            type_element = etree.find('os').find('type')
+            if 'machine' in type_element.keys():
+                type_element.set('machine', 'pc')
+                logging.debug('XML updated with machine "pc"')
+        except AttributeError:
+            logging.warning('Failure to set XML machine type')
+
+        # qemu-kvm path may differ per system, need to detect it and update xml
+        linux_ver = distro.linux_distribution()[0]
+        if linux_ver == 'Fedora':
+            qemu_path = '/usr/bin/qemu-kvm'
+        else:
+            qemu_path = '/usr/libexec/qemu-kvm'
+
+        try:
+            etree.find('devices').find('emulator').text = qemu_path
+            logging.debug('XML updated with emulator location: '
+                          '{}'.format(qemu_path))
+            xml = ET.tostring(etree).decode('utf-8')
+        except AttributeError:
+            logging.warning('Failure to update XML qemu path')
+
+        if disk_path:
+            try:
+                disk_element = etree.find('devices').find('disk').find(
+                    'source')
+                disk_element.set('file', disk_path)
+                logging.debug('XML updated with file path: {}'.format(
+                    disk_path))
+            except AttributeError:
+                logging.error('Failure to parse XML and set disk type')
+                raise OvercloudNodeException(
+                    'Unable to set new disk path in xml {}'.format(xml))
+
+        return ET.tostring(etree).decode('utf-8')
+
+    def create(self, src_disk):
+        # copy disk to pool and get new disk location
+        logging.debug('Preparing disk image')
+        self._configure_disk(src_disk)
+        logging.debug('Parsing node XML from {}'.format(self.node_xml_file))
+        with open(self.node_xml_file, 'r') as fh:
+            self.node_xml = fh.read()
+        # if machine is not pc we need to set, also need to update qemu-kvm and
+        # storage location
+        self.node_xml = self._update_xml(self.node_xml, self.disk_img)
+        logging.info('Creating node {} in libvirt'.format(self.name))
+        self.vm = self.conn.defineXML(self.node_xml)
+
+    def start(self):
+        """
+        Boot node in libvirt
+        :return:
+        """
+        try:
+            self.vm.create()
+            logging.info('Node {} started'.format(self.name))
+        except libvirt.libvirtError as e:
+            logging.error('Failed to start domain: {}'.format(self.name))
+            raise OvercloudNodeException('Failed to start VM. Reason: '
+                                         '{}'.format(e))
 
--- /dev/null
+<network connections='1' ipv6='yes'>
+  <name>admin</name>
+  <uuid>761c34f8-2a72-4205-8e69-5ed6626c6efa</uuid>
+  <forward mode='bridge'/>
+  <bridge name='br-admin'/>
+  <virtualport type='openvswitch'/>
+</network>
 
--- /dev/null
+<domain type='kvm'>
+  <name>baremetal0</name>
+  <uuid>25bf15b6-130c-4bca-87af-e5cbc14bb454</uuid>
+  <memory unit='KiB'>12582912</memory>
+  <currentMemory unit='KiB'>12582912</currentMemory>
+  <vcpu placement='static'>4</vcpu>
+  <resource>
+    <partition>/machine</partition>
+  </resource>
+  <os>
+    <type arch='x86_64' machine='pc-i440fx-rhel7.0.0'>hvm</type>
+    <boot dev='hd'/>
+    <bootmenu enable='no'/>
+  </os>
+  <features>
+    <acpi/>
+    <apic/>
+    <pae/>
+  </features>
+  <cpu mode='host-passthrough'/>
+  <clock offset='utc'/>
+  <on_poweroff>destroy</on_poweroff>
+  <on_reboot>restart</on_reboot>
+  <on_crash>restart</on_crash>
+  <devices>
+    <emulator>/usr/libexec/qemu-kvm</emulator>
+    <disk type='file' device='disk'>
+      <driver name='qemu' type='qcow2' cache='unsafe'/>
+      <source file='/home/images/baremetal0.qcow2'/>
+      <target dev='sda' bus='sata'/>
+      <address type='drive' controller='0' bus='0' target='0' unit='0'/>
+    </disk>
+    <controller type='scsi' index='0' model='virtio-scsi'>
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
+    </controller>
+    <controller type='usb' index='0'>
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/>
+    </controller>
+    <controller type='pci' index='0' model='pci-root'/>
+    <controller type='sata' index='0'>
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x0'/>
+    </controller>
+    <interface type='bridge'>
+      <mac address='00:5b:06:25:0c:dc'/>
+      <source bridge='br-admin'/>
+      <virtualport type='openvswitch'>
+        <parameters interfaceid='04b63cb9-21a9-4385-bbd6-df677a5eeecf'/>
+      </virtualport>
+      <model type='virtio'/>
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
+    </interface>
+    <serial type='pty'>
+      <target port='0'/>
+    </serial>
+    <console type='pty'>
+      <target type='serial' port='0'/>
+    </console>
+    <input type='mouse' bus='ps2'/>
+    <input type='keyboard' bus='ps2'/>
+    <graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'>
+      <listen type='address' address='127.0.0.1'/>
+    </graphics>
+    <video>
+      <model type='cirrus' vram='16384' heads='1' primary='yes'/>
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
+    </video>
+    <memballoon model='virtio'>
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>
+    </memballoon>
+  </devices>
+  <seclabel type='dynamic' model='selinux' relabel='yes'/>
+  <seclabel type='dynamic' model='dac' relabel='yes'/>
+</domain>
 
--- /dev/null
+---
+servers:
+  overcloud-controller-0.opnfvlf.org:
+    address: 192.0.2.28
+    orig-ctl-mac: 00:5b:06:25:0c:dc
+    ovs-controller: tcp:192.0.2.28:6653
+    ovs-managers:
+      - ptcp:6639:127.0.0.1
+      - tcp:192.0.2.28:6640
+    type: controller
+    user: heat-admin
+    vNode-name: baremetal0
 
--- /dev/null
+OPNFV_SNAP_URL=artifacts.opnfv.org/apex/master/noha/apex-csit-snap-2018-08-05.tar.gz
+OPNFV_SNAP_SHA512SUM=bb0c6fa0e675dcb39cfad11d81bb99f309d5cfc236e36a74d05ee813584f3e5bb92aa23dec775846317b75d574f8c86186c666f78a299c24fb68849897bdd4bc
 
         assert output is not None
 
     def test_open_invalid_webpage(self):
-        assert_raises(urllib.request.URLError, utils.open_webpage,
+        assert_raises(exceptions.FetchException, utils.open_webpage,
                       'http://inv4lIdweb-page.com')
 
     @patch('builtins.open', a_mock_open)
 
 ##############################################################################
 
 import argparse
+import os
 import unittest
 
 from mock import patch
 
 from apex.common.exceptions import ApexDeployException
 from apex.common.constants import DEFAULT_OS_VERSION
-from apex.deploy import deploy_quickstart
 from apex.deploy import validate_cross_settings
 from apex.deploy import build_vms
 from apex.deploy import create_deploy_parser
 from apex.deploy import validate_deploy_args
 from apex.deploy import main
+from apex.tests.constants import TEST_DUMMY_CONFIG
 
 from nose.tools import (
     assert_is_instance,
     def teardown(self):
         """This method is run once after _each_ test method is executed"""
 
-    def test_deloy_quickstart(self):
-        deploy_quickstart(None, None, None)
-
     def test_validate_cross_settings(self):
         deploy_settings = {'deploy_options': {'dataplane': 'ovs'}}
         net_settings = Mock()
         args = Mock()
         args.inventory_file = None
         args.virtual = True
+        args.snapshot = False
+        validate_deploy_args(args)
+
+    def test_validate_snapshot_deploy_args(self):
+        args = Mock()
+        args.deploy_settings_file = os.path.join(TEST_DUMMY_CONFIG,
+                                                 'dummy-deploy-settings.yaml')
+        args.inventory_file = None
+        args.virtual = True
+        args.snapshot = True
         validate_deploy_args(args)
 
     def test_validate_deploy_args_no_virt_no_inv(self):
         args = Mock()
         args.inventory_file = 'file_name'
         args.virtual = False
+        args.snapshot = False
         assert_raises(ApexDeployException, validate_deploy_args, args)
 
     @patch('apex.deploy.os.path')
         args = Mock()
         args.inventory_file = None
         args.virtual = True
+        args.snapshot = False
         assert_raises(ApexDeployException, validate_deploy_args, args)
 
     def test_validate_deploy_args_virt_and_inv_file(self):
         args = Mock()
         args.inventory_file = 'file_name'
         args.virtual = True
+        args.snapshot = False
         assert_raises(ApexDeployException, validate_deploy_args, args)
 
     @patch('apex.deploy.ApexDeployment')
         args.virtual = False
         args.quickstart = False
         args.debug = False
+        args.snapshot = False
         args.upstream = True
         net_sets = mock_net_sets.return_value
         net_sets.enabled_network_list = ['external']
         mock_parsers.parse_nova_output.return_value = {'testnode1': 'test'}
         main()
 
+    @patch('apex.deploy.SnapshotDeployment')
     @patch('apex.deploy.validate_cross_settings')
     @patch('apex.deploy.virt_utils')
     @patch('apex.deploy.utils')
     @patch('apex.deploy.os')
     @patch('apex.deploy.create_deploy_parser')
     @patch('builtins.open', a_mock_open, create=True)
-    def test_main_qs(self, mock_parser, mock_os, mock_deploy,
-                     mock_net_sets, mock_net_env, mock_inv, mock_utils,
-                     mock_virt_utils, mock_cross):
+    def test_main_snapshot(self, mock_parser, mock_os, mock_deploy,
+                           mock_net_sets, mock_net_env, mock_inv, mock_utils,
+                           mock_virt_utils, mock_cross, mock_snap_deployment):
         args = mock_parser.return_value.parse_args.return_value
         args.virtual = False
-        args.quickstart = True
+        args.snapshot = True
         args.debug = True
         main()
+        mock_snap_deployment.assert_called()
 
     @patch('apex.deploy.ApexDeployment')
     @patch('apex.deploy.uc_builder')
         args.virt_compute_ram = None
         args.virt_default_ram = 12
         args.upstream = True
+        args.snapshot = False
         net_sets = mock_net_sets.return_value
         net_sets.enabled_network_list = ['admin']
         deploy_sets = mock_deploy_sets.return_value
         args.virt_compute_ram = None
         args.virt_default_ram = 12
         args.upstream = True
+        args.snapshot = False
         net_sets = mock_net_sets.return_value
         net_sets.enabled_network_list = ['admin']
         deploy_sets = mock_deploy_sets.return_value
         args.quickstart = False
         args.debug = False
         args.upstream = False
+        args.snapshot = False
         net_sets = mock_net_sets.return_value
         net_sets.enabled_network_list = ['external']
         net_sets.__getitem__.side_effect = net_sets_dict.__getitem__
 
--- /dev/null
+##############################################################################
+# Copyright (c) 2018 Tim Rozet (trozet@redhat.com) (Red Hat)
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+from mock import patch
+import os
+import unittest
+import urllib.request
+
+from apex.common import exceptions as exc
+from apex.deployment.snapshot import SnapshotDeployment
+from apex.settings.deploy_settings import DeploySettings
+from apex.tests.constants import TEST_DUMMY_CONFIG
+
+DUMMY_SNAP_DIR = '/tmp/dummy_cache'
+
+
+class TestSnapshotDeployment(unittest.TestCase):
+    @classmethod
+    def setup_class(cls):
+        """This method is run once for each class before any tests are run"""
+
+    @classmethod
+    def teardown_class(cls):
+        """This method is run once for each class _after_ all tests are run"""
+
+    def setup(self):
+        """This method is run once before _each_ test method is executed"""
+
+    def teardown(self):
+        """This method is run once after _each_ test method is executed"""
+
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_init(self, mock_deploy_snap, mock_libvirt_open, mock_pull_snap):
+
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=True, all_in_one=False)
+        snap_dir = os.path.join(DUMMY_SNAP_DIR, 'queens', 'noha')
+        self.assertEqual(d.snap_cache_dir, snap_dir)
+        mock_pull_snap.assert_called()
+        mock_deploy_snap.assert_called()
+        self.assertEqual(d.ha_ext, 'noha')
+
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_init_allinone_no_fetch(self, mock_deploy_snap, mock_libvirt_open,
+                                    mock_pull_snap):
+
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=True)
+        snap_dir = os.path.join(DUMMY_SNAP_DIR, 'queens', 'noha-allinone')
+        self.assertEqual(d.snap_cache_dir, snap_dir)
+        mock_pull_snap.assert_not_called()
+        mock_deploy_snap.assert_called()
+        self.assertEqual(d.ha_ext, 'noha-allinone')
+
+    @patch('apex.deployment.snapshot.utils.fetch_upstream_and_unpack')
+    @patch('apex.deployment.snapshot.utils.fetch_properties')
+    def test_pull_snapshot_is_latest(self, mock_fetch_props,
+                                     mock_fetch_artifact):
+        mock_fetch_props.return_value = {
+            'OPNFV_SNAP_URL': 'artifacts.opnfv.org/apex/master/noha/'
+                              'apex-csit-snap-2018-08-05.tar.gz',
+            'OPNFV_SNAP_SHA512SUM': 'bb0c6fa0e675dcb39cfad11d81bb99f309d5cfc23'
+                                    '6e36a74d05ee813584f3e5bb92aa23dec77584631'
+                                    '7b75d574f8c86186c666f78a299c24fb68849897b'
+                                    'dd4bc'
+        }
+        SnapshotDeployment.pull_snapshot('http://dummy_url',
+                                         TEST_DUMMY_CONFIG)
+        mock_fetch_artifact.assert_not_called()
+
+    @patch('apex.deployment.snapshot.utils.fetch_upstream_and_unpack')
+    @patch('apex.deployment.snapshot.utils.fetch_properties')
+    def test_pull_snapshot_fetch_props_failure(self, mock_fetch_props,
+                                               mock_fetch_artifact):
+        mock_fetch_props.side_effect = exc.FetchException
+        self.assertRaises(exc.FetchException,
+                          SnapshotDeployment.pull_snapshot,
+                          'http://dummy_url', TEST_DUMMY_CONFIG)
+
+    @patch('apex.deployment.snapshot.utils.fetch_upstream_and_unpack')
+    @patch('apex.deployment.snapshot.utils.fetch_properties')
+    def test_pull_snapshot_is_not_latest(self, mock_fetch_props,
+                                         mock_fetch_artifact):
+        mock_fetch_props.side_effect = [{
+            'OPNFV_SNAP_URL': 'artifacts.opnfv.org/apex/master/noha/'
+                              'apex-csit-snap-2018-08-05.tar.gz',
+            'OPNFV_SNAP_SHA512SUM': '123c6fa0e675dcb39cfad11d81bb99f309d5cfc23'
+                                    '6e36a74d05ee813584f3e5bb92aa23dec77584631'
+                                    '7b75d574f8c86186c666f78a299c24fb68849897b'
+                                    'dd4bc'},
+            {
+            'OPNFV_SNAP_URL': 'artifacts.opnfv.org/apex/master/noha/'
+                              'apex-csit-snap-2018-08-05.tar.gz',
+            'OPNFV_SNAP_SHA512SUM': 'bb0c6fa0e675dcb39cfad11d81bb99f309d5cfc23'
+                                    '6e36a74d05ee813584f3e5bb92aa23dec77584631'
+                                    '7b75d574f8c86186c666f78a299c24fb68849897b'
+                                    'dd4bc'}]
+        SnapshotDeployment.pull_snapshot('http://dummy_url',
+                                         TEST_DUMMY_CONFIG)
+        mock_fetch_artifact.assert_called()
+
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_create_networks(self, mock_deploy_snap, mock_libvirt_open,
+                             mock_pull_snap, mock_oc_node):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        conn = mock_libvirt_open('qemu:///system')
+        d.create_networks()
+        conn.networkCreateXML.assert_called()
+
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_create_networks_invalid_cache(self, mock_deploy_snap,
+                                           mock_libvirt_open, mock_pull_snap,
+                                           mock_oc_node):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = '/doesnotexist/'
+        self.assertRaises(exc.SnapshotDeployException, d.create_networks)
+
+    @patch('apex.deployment.snapshot.fnmatch')
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_create_networks_no_net_xmls(self, mock_deploy_snap,
+                                         mock_libvirt_open, mock_pull_snap,
+                                         mock_oc_node, mock_fnmatch):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = '/doesnotexist/'
+        mock_fnmatch.filter.return_value = []
+        self.assertRaises(exc.SnapshotDeployException, d.create_networks)
+
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_parse_and_create_nodes(self, mock_deploy_snap, mock_libvirt_open,
+                                    mock_pull_snap, mock_oc_node):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        d.parse_and_create_nodes()
+        node.start.assert_called()
+        self.assertListEqual([node], d.oc_nodes)
+
+    @patch('apex.deployment.snapshot.utils.parse_yaml')
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_parse_and_create_nodes_invalid_node_yaml(
+            self, mock_deploy_snap, mock_libvirt_open, mock_pull_snap,
+            mock_oc_node, mock_parse_yaml):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        mock_parse_yaml.return_value = {'blah': 'dummy'}
+        self.assertRaises(exc.SnapshotDeployException,
+                          d.parse_and_create_nodes)
+        node.start.assert_not_called()
+
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_get_controllers(self, mock_deploy_snap, mock_libvirt_open,
+                             mock_pull_snap, mock_oc_node):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        node.role = 'controller'
+        d.oc_nodes = [node]
+        self.assertListEqual(d.get_controllers(), [node])
+
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_get_controllers_none(self, mock_deploy_snap, mock_libvirt_open,
+                                  mock_pull_snap, mock_oc_node):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        node.role = 'compute'
+        d.oc_nodes = [node]
+        self.assertListEqual(d.get_controllers(), [])
+
+    @patch('apex.deployment.snapshot.SnapshotDeployment.get_controllers')
+    @patch('apex.deployment.snapshot.time')
+    @patch('apex.deployment.snapshot.socket')
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_is_openstack_up(self, mock_deploy_snap, mock_libvirt_open,
+                             mock_pull_snap, mock_oc_node, mock_socket,
+                             mock_time, mock_get_ctrls):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        node.ip = '123.123.123.123'
+        node.name = 'dummy-controller-0'
+        mock_get_ctrls.return_value = [node]
+        sock = mock_socket.socket(mock_socket.AF_INET, mock_socket.SOCK_STREAM)
+        sock.connect_ex.return_value = 0
+        self.assertTrue(d.is_service_up('openstack'))
+
+    @patch('apex.deployment.snapshot.SnapshotDeployment.get_controllers')
+    @patch('apex.deployment.snapshot.time')
+    @patch('apex.deployment.snapshot.socket')
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_is_openstack_up_false(self, mock_deploy_snap, mock_libvirt_open,
+                                   mock_pull_snap, mock_oc_node, mock_socket,
+                                   mock_time, mock_get_ctrls):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        node.ip = '123.123.123.123'
+        node.name = 'dummy-controller-0'
+        mock_get_ctrls.return_value = [node]
+        sock = mock_socket.socket(mock_socket.AF_INET, mock_socket.SOCK_STREAM)
+        sock.connect_ex.return_value = 1
+        self.assertFalse(d.is_service_up('openstack'))
+
+    @patch('apex.deployment.snapshot.SnapshotDeployment.get_controllers')
+    @patch('apex.deployment.snapshot.time')
+    @patch('apex.deployment.snapshot.utils')
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_is_opendaylight_up(self, mock_deploy_snap, mock_libvirt_open,
+                                mock_pull_snap, mock_oc_node, mock_utils,
+                                mock_time, mock_get_ctrls):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        node.ip = '123.123.123.123'
+        node.name = 'dummy-controller-0'
+        mock_get_ctrls.return_value = [node]
+        mock_utils.open_webpage.return_value = 0
+        self.assertTrue(d.is_service_up('opendaylight'))
+
+    @patch('apex.deployment.snapshot.SnapshotDeployment.get_controllers')
+    @patch('apex.deployment.snapshot.time')
+    @patch('apex.deployment.snapshot.utils')
+    @patch('apex.deployment.snapshot.OvercloudNode')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.deploy_snapshot')
+    def test_is_opendaylight_up_false(self, mock_deploy_snap,
+                                      mock_libvirt_open, mock_pull_snap,
+                                      mock_oc_node, mock_utils,
+                                      mock_time, mock_get_ctrls):
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        d = SnapshotDeployment(deploy_settings=ds,
+                               snap_cache_dir=DUMMY_SNAP_DIR,
+                               fetch=False, all_in_one=False)
+        d.snap_cache_dir = TEST_DUMMY_CONFIG
+        node = mock_oc_node()
+        node.ip = '123.123.123.123'
+        node.name = 'dummy-controller-0'
+        mock_get_ctrls.return_value = [node]
+        mock_utils.open_webpage.side_effect = urllib.request.URLError(
+            reason='blah')
+        self.assertFalse(d.is_service_up('opendaylight'))
+
+    @patch('apex.deployment.snapshot.os.path.isfile')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.is_service_up')
+    @patch('apex.deployment.snapshot.SnapshotDeployment'
+           '.parse_and_create_nodes')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.create_networks')
+    def test_deploy_snapshot(self, mock_create_networks, mock_libvirt_open,
+                             mock_pull_snap, mock_parse_create,
+                             mock_service_up, mock_is_file):
+        mock_is_file.return_value = True
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        SnapshotDeployment(deploy_settings=ds, snap_cache_dir=DUMMY_SNAP_DIR,
+                           fetch=False, all_in_one=False)
+        mock_parse_create.assert_called()
+        mock_create_networks.assert_called()
+        mock_service_up.assert_called()
+
+    @patch('apex.deployment.snapshot.os.path.isfile')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.is_service_up')
+    @patch('apex.deployment.snapshot.SnapshotDeployment'
+           '.parse_and_create_nodes')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.pull_snapshot')
+    @patch('apex.deployment.snapshot.libvirt.open')
+    @patch('apex.deployment.snapshot.SnapshotDeployment.create_networks')
+    def test_deploy_snapshot_services_down(self, mock_create_networks,
+                                           mock_libvirt_open,
+                                           mock_pull_snap, mock_parse_create,
+                                           mock_service_up, mock_is_file):
+        mock_is_file.return_value = True
+        ds_file = os.path.join(TEST_DUMMY_CONFIG, 'dummy-deploy-settings.yaml')
+        ds = DeploySettings(ds_file)
+        mock_service_up.return_value = False
+        self.assertRaises(exc.SnapshotDeployException,
+                          SnapshotDeployment,
+                          ds, DUMMY_SNAP_DIR, False, False)
+
+        mock_service_up.side_effect = [True, False]
+        self.assertRaises(exc.SnapshotDeployException,
+                          SnapshotDeployment,
+                          ds, DUMMY_SNAP_DIR, False, False)
 
--- /dev/null
+##############################################################################
+# Copyright (c) 2018 Tim Rozet (trozet@redhat.com) (Red Hat)
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+from libvirt import libvirtError
+from mock import patch
+from mock import MagicMock
+import os
+import unittest
+import urllib.request
+
+from apex.common import exceptions as exc
+from apex.overcloud.node import OvercloudNode
+from apex.settings.deploy_settings import DeploySettings
+from apex.tests.constants import TEST_DUMMY_CONFIG
+
+DUMMY_SNAP_DIR = '/tmp/dummy_cache'
+
+
+class TestSnapshotDeployment(unittest.TestCase):
+    @classmethod
+    def setup_class(cls):
+        """This method is run once for each class before any tests are run"""
+
+    @classmethod
+    def teardown_class(cls):
+        """This method is run once for each class _after_ all tests are run"""
+
+    def setup(self):
+        """This method is run once before _each_ test method is executed"""
+
+    def teardown(self):
+        """This method is run once after _each_ test method is executed"""
+
+    @patch('apex.overcloud.node.OvercloudNode.create')
+    @patch('apex.overcloud.node.os.path.isfile')
+    @patch('apex.overcloud.node.libvirt.open')
+    def test_init(self, mock_libvirt_open, mock_is_file, mock_node_create):
+        mock_is_file.return_value = True
+        OvercloudNode(role='controller', ip='123.123.123.123',
+                      ovs_ctrlrs=None, ovs_mgrs=None,
+                      name='dummy-controller-0', node_xml='dummynode.xml',
+                      disk_img='dummy.qcow2')
+        mock_node_create.assert_called()
+
+    @patch('apex.overcloud.node.OvercloudNode.create')
+    @patch('apex.overcloud.node.libvirt.open')
+    def test_init_invalid_files(self, mock_libvirt_open, mock_node_create):
+        self.assertRaises(exc.OvercloudNodeException,
+                          OvercloudNode, 'controller', '123.123.123',
+                          None, None, 'dummy-controller-0', 'dummynode.xml',
+                          'dummy.qcow2')
+
+    @patch('apex.overcloud.node.shutil.copyfile')
+    @patch('apex.overcloud.node.OvercloudNode.create')
+    @patch('apex.overcloud.node.os.path.isfile')
+    @patch('apex.overcloud.node.libvirt.open')
+    def test_configure_disk(self, mock_libvirt_open, mock_is_file,
+                            mock_node_create, mock_copy):
+        mock_is_file.return_value = True
+        node = OvercloudNode(role='controller', ip='123.123.123.123',
+                             ovs_ctrlrs=None, ovs_mgrs=None,
+                             name='dummy-controller-0',
+                             node_xml='dummynode.xml',
+                             disk_img='dummy.qcow2')
+        conn = mock_libvirt_open.return_value
+        conn.storagePoolLookupByName.return_value.XMLDesc.return_value = """
+        <pool type='dir'>
+          <target>
+            <path>/var/lib/libvirt/images</path>
+          </target>
+        </pool>
+        """
+        node._configure_disk('dummy.qcow2')
+        mock_copy.assert_called()
+        self.assertEqual(node.disk_img, '/var/lib/libvirt/images/dummy.qcow2')
+
+    @patch('apex.overcloud.node.shutil.copyfile')
+    @patch('apex.overcloud.node.OvercloudNode.create')
+    @patch('apex.overcloud.node.os.path.isfile')
+    @patch('apex.overcloud.node.libvirt.open')
+    def test_configure_disk_bad_path(self, mock_libvirt_open, mock_is_file,
+                                     mock_node_create, mock_copy):
+        mock_is_file.return_value = True
+        node = OvercloudNode(role='controller', ip='123.123.123.123',
+                             ovs_ctrlrs=None, ovs_mgrs=None,
+                             name='dummy-controller-0',
+                             node_xml='dummynode.xml',
+                             disk_img='dummy.qcow2')
+        conn = mock_libvirt_open.return_value
+        conn.storagePoolLookupByName.return_value.XMLDesc.return_value = """
+        <pool type='dir'>
+          <target>
+          </target>
+        </pool>
+        """
+        self.assertRaises(exc.OvercloudNodeException,
+                          node._configure_disk, 'dummy.qcow2')
+
+    @patch('apex.overcloud.node.shutil.copyfile')
+    @patch('apex.overcloud.node.OvercloudNode.create')
+    @patch('apex.overcloud.node.os.path.isfile')
+    @patch('apex.overcloud.node.libvirt.open')
+    def test_configure_disk_no_pool(self, mock_libvirt_open, mock_is_file,
+                                    mock_node_create, mock_copy):
+        mock_is_file.return_value = True
+        node = OvercloudNode(role='controller', ip='123.123.123.123',
+                             ovs_ctrlrs=None, ovs_mgrs=None,
+                             name='dummy-controller-0',
+                             node_xml='dummynode.xml',
+                             disk_img='dummy.qcow2')
+        conn = mock_libvirt_open.return_value
+        conn.storagePoolLookupByName.return_value = None
+        self.assertRaises(exc.OvercloudNodeException,
+                          node._configure_disk, 'dummy.qcow2')
+
+    @patch('apex.overcloud.node.distro.linux_distribution')
+    def test_update_xml(self, mock_linux_distro):
+        mock_linux_distro.return_value = ['Fedora']
+        xml_file = os.path.join(TEST_DUMMY_CONFIG, 'baremetal0.xml')
+        with open(xml_file, 'r') as fh:
+            xml = fh.read()
+        new_xml = OvercloudNode._update_xml(
+            xml=xml, disk_path='/dummy/disk/path/blah.qcow2')
+        self.assertIn('/dummy/disk/path/blah.qcow2', new_xml)
+        self.assertIn('/usr/bin/qemu-kvm', new_xml)
+
+    @patch('apex.overcloud.node.distro.linux_distribution')
+    def test_update_xml_no_disk(self, mock_linux_distro):
+        mock_linux_distro.return_value = ['Fedora']
+        xml_file = os.path.join(TEST_DUMMY_CONFIG, 'baremetal0.xml')
+        with open(xml_file, 'r') as fh:
+            xml = fh.read()
+        new_xml = OvercloudNode._update_xml(xml=xml)
+        self.assertIn('/home/images/baremetal0.qcow2', new_xml)
+        self.assertIn('/usr/bin/qemu-kvm', new_xml)
+
+    @patch('apex.overcloud.node.OvercloudNode._update_xml')
+    @patch('apex.overcloud.node.OvercloudNode._configure_disk')
+    @patch('apex.overcloud.node.libvirt.open')
+    @patch('apex.overcloud.node.os.path.isfile')
+    def test_create(self, mock_isfile, mock_libvirt_conn, mock_configure_disk,
+                    mock_update_xml):
+        mock_isfile.return_value = True
+        domain = mock_libvirt_conn.return_value.defineXML.return_value
+        node = OvercloudNode(role='controller', ip='123.123.123.123',
+                             ovs_ctrlrs=None, ovs_mgrs=None,
+                             name='dummy-controller-0',
+                             node_xml=os.path.join(TEST_DUMMY_CONFIG,
+                                                   'baremetal0.xml'),
+                             disk_img='dummy.qcow2')
+        self.assertIs(node.vm, domain)
+
+    @patch('apex.overcloud.node.OvercloudNode._update_xml')
+    @patch('apex.overcloud.node.OvercloudNode._configure_disk')
+    @patch('apex.overcloud.node.libvirt.open')
+    @patch('apex.overcloud.node.os.path.isfile')
+    def test_start(self, mock_isfile, mock_libvirt_conn, mock_configure_disk,
+                   mock_update_xml):
+        mock_isfile.return_value = True
+        domain = mock_libvirt_conn.return_value.defineXML.return_value
+        node = OvercloudNode(role='controller', ip='123.123.123.123',
+                             ovs_ctrlrs=None, ovs_mgrs=None,
+                             name='dummy-controller-0',
+                             node_xml=os.path.join(TEST_DUMMY_CONFIG,
+                                                   'baremetal0.xml'),
+                             disk_img='dummy.qcow2')
+        node.start()
+        domain.create.assert_called()
+
+    @patch('apex.overcloud.node.OvercloudNode._update_xml')
+    @patch('apex.overcloud.node.OvercloudNode._configure_disk')
+    @patch('apex.overcloud.node.libvirt.open')
+    @patch('apex.overcloud.node.os.path.isfile')
+    def test_start_fail(self, mock_isfile, mock_libvirt_conn,
+                        mock_configure_disk, mock_update_xml):
+        mock_isfile.return_value = True
+        domain = mock_libvirt_conn.return_value.defineXML.return_value
+        domain.create.side_effect = libvirtError('blah')
+        node = OvercloudNode(role='controller', ip='123.123.123.123',
+                             ovs_ctrlrs=None, ovs_mgrs=None,
+                             name='dummy-controller-0',
+                             node_xml=os.path.join(TEST_DUMMY_CONFIG,
+                                                   'baremetal0.xml'),
+                             disk_img='dummy.qcow2')
+        self.assertRaises(exc.OvercloudNodeException, node.start)
 
 
 Deploying virtually is an alternative deployment method to bare metal, where
 only a single bare metal Jump Host server is required to execute deployment.
-In this deployment type, the Jump Host server will host the undercloud VM along
-with any number of OPNFV overcloud control/compute nodes.  This deployment type
-is useful when physical resources are constrained, or there is a desire to
-deploy a temporary sandbox environment.
+This deployment type is useful when physical resources are constrained, or
+there is a desire to deploy a temporary sandbox environment.
+
+With virtual deployments, two deployment options are offered. The first is a
+standard deployment where the Jump Host server will host the undercloud VM along
+with any number of OPNFV overcloud control/compute nodes. This follows the same
+deployment workflow as baremetal, and can take between 1 to 2 hours to complete.
+
+The second option is to use snapshot deployments. Snapshots are saved disk images
+of previously deployed OPNFV upstream. These snapshots are promoted daily and contain
+and already deployed OPNFV environment that has passed a series of tests. The
+advantage of the snapshot is that it deploys in less than 10 minutes. Another
+major advantage is that the snapshots work on both CentOS and Fedora OS. Note:
+Fedora support is only tested via PIP installation at this time and not via RPM.
+
+Standard Deployment Overview
+----------------------------
 
 The virtual deployment operates almost the same way as the bare metal
 deployment with a few differences mainly related to power management.
 modification.  Customizations are welcome but not needed if a generic set of
 network settings are acceptable.
 
+Snapshot Deployment Overview
+----------------------------
+
+Snapshot deployments use the same ``opnfv-deploy`` CLI as standard deployments.
+The snapshot deployment will use a cache in order to store snapshots that are
+downloaded from the internet at deploy time. This caching avoids re-downloading
+the same artifact between deployments. The snapshot deployment recreates the same
+network and libvirt setup as would have been provisioned by the Standard
+deployment, with the exception that there is no undercloud VM. The snapshot
+deployment will give the location of the RC file to use in order to interact
+with the Overcloud directly from the jump host.
+
+Snapshots come in different topology flavors. One is able to deploy either HA
+(3 Control, 2 Computes, no-HA (1 Control, 2 Computes), or all-in-one
+(1 Control/Compute. The snapshot deployment itself is always done with the
+os-odl-nofeature-* scenario.
+
 Installation Guide - Virtual Deployment
 =======================================
 
 
 Follow the instructions in the `Install Bare Metal Jump Host`_ section.
 
-Running ``opnfv-deploy``
-------------------------
+Running ``opnfv-deploy`` for Standard Deployment
+------------------------------------------------
 
 You are now ready to deploy OPNFV!
 ``opnfv-deploy`` has virtual deployment capability that includes all of
 3.  When the deployment is complete the IP for the undercloud and a url for the
     OpenStack dashboard will be displayed
 
+Running ``opnfv-deploy`` for Snapshot Deployment
+------------------------------------------------
+
+Deploying snapshots requires enough disk space to cache snapshot archives, as well
+as store VM disk images per deployment. The snapshot cache directory can be
+configured at deploy time. Ensure a directory is used on a partition with enough
+space for about 20GB. Additionally, Apex will attempt to detect the default
+libvirt storage pool on the jump host. This is typically '/var/lib/libvirt/images'.
+On default CentOS installations, this path will resolve to the /root partition,
+which is only around 50GB. Therefore, ensure that the path for the default storage
+pool has enough space to hold the VM backing storage (approx 4GB per VM). Note,
+each Overcloud VM disk size is set to 40GB, however Libvirt grows these disks
+dynamically. Due to this only 4GB will show up at initial deployment, but the disk
+may grow from there up to 40GB.
+
+The new arguments to deploy snapshots include:
+
+  - `--snapshot`: Enables snapshot deployments
+  - `--snap-cache`: Indicates the directory to use for caching artifacts
+
+An example deployment command is:
+
+.. code-block::
+
+  opnfv-deploy -d ../config/deploy/os-odl-queens-noha.yaml --snapshot
+  --snap-cache /home/trozet/snap_cache --virtual-computes 0 --no-fetch
+
+In the above example, several of the Standard Deployment arguments are still
+used to deploy snapshots:
+
+  - `-d`: Deploy settings are used to determine OpenStack version of snapshots
+    to use as well as the topology
+  - `--virtual-computes` - When set to 0, it indicates to Apex to use an
+    all-in-one snapshot
+  - `--no-fetch` - Can be used to disable fetching latest snapshot artifact
+    from upstream and use the latest found in `--snap-cache`
+
 Verifying the Setup - VMs
 -------------------------
 
 
         - libguestfs-tools
         - python-netaddr
         - python2-pip
+      when: ansible_distribution == 'CentOS'
+    - dnf:
+        name: "{{ item }}"
+        state: present
+      with_items:
+        - libselinux-python
+        - python-lxml
+        - libvirt-python
+        - libguestfs-tools
+        - python-netaddr
+        - python2-pip
+      when: ansible_distribution == 'Fedora'
     - pip:
         name: ansible-modules-hashivault,hvac,Jinja2
         state: latest
         executable: pip2
+      when: not snapshot
     - sysctl:
         name: net.ipv4.ip_forward
         state: present
         xml: '{{ lookup("template", "virsh_network_default.xml.j2") }}'
         state: active
         autostart: yes
+      when: not snapshot
     - openvswitch_bridge:
         bridge: 'br-{{ item }}'
         state: present
       with_items: '{{ virsh_enabled_networks }}'
+    - name: 'Configure IP on bridge'
+      shell: 'ip addr add  192.0.2.99/24 dev br-{{ item }}'
+      with_items: '{{ virsh_enabled_networks }}'
+      when: snapshot
+    - name: 'Bring up bridge'
+      shell: 'ip link set up br-{{ item }}'
+      with_items: '{{ virsh_enabled_networks }}'
+      when: snapshot
     - virt_net:
         state: present
         name: '{{ item }}'
         xml: '{{ lookup("template", "virsh_network_ovs.xml.j2") }}'
       with_items: '{{ virsh_enabled_networks }}'
+      when: not snapshot
     - virt_net:
         state: active
         name: '{{ item }}'
         autostart: yes
       with_items: '{{ virsh_enabled_networks }}'
+      when: not snapshot
     - virt_pool:
         name: default
         autostart: yes
         state: present
     - name: Generate SSH key for root if missing
       shell: test -e ~/.ssh/id_rsa || ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa
+      when: not snapshot
     - name: Check that /u/l/python3.4/site-packages/virtualbmc/vbmc.py exists
       stat:
         path: /usr/lib/python3.4/site-packages/virtualbmc/vbmc.py
       register: vbmc_py
+      when: not snapshot
     - name: Manually patch vmbc to work with python3.x
       lineinfile:
         line: "                conn.defineXML(ET.tostring(tree, encoding='unicode'))"
         regexp: "tostring"
         path: /usr/lib/python3.4/site-packages/virtualbmc/vbmc.py
       when: vbmc_py.stat.exists == True
+      when: not snapshot
     - name: Add ssh retry to Ansible config
       ini_file:
         path: /etc/ansible/ansible.cfg
 
   --cover-package=apex \
   --cover-xml \
   --cover-min-percentage 95 \
-  apex/tests
+  {posargs}
   coverage report
 
 [testenv:pep8]