1
# Copyright 2013 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Management of iSCSI targets for the ephemerals import script."""
6
from __future__ import (
30
from textwrap import dedent
32
from provisioningserver.kernel_opts import (
33
ISCSI_TARGET_NAME_PREFIX,
36
from provisioningserver.utils import (
38
call_capture_and_check,
43
# Basic tgt-admin command line.
44
TGT_ADMIN = ["tgt-admin", "--conf", "/etc/tgt/targets.conf"]
46
# Template for the tgt.conf file in the data directory.
47
DATA_DIR_TGT_CONF_TEMPLATE = dedent("""\
52
# Template for individual tgt.conf files in target directories.
53
TGT_CONF_TEMPLATE = dedent("""\
54
<target {prefix}:{{target_name}}>
56
backing-store "{{image}}"
58
""").format(prefix=ISCSI_TARGET_NAME_PREFIX)
60
INFO_TEMPLATE = dedent("""\
69
def tgt_conf_d(data_dir):
70
"""Return the path of a data directory's configuration directory."""
71
return os.path.abspath(os.path.join(data_dir, 'tgt.conf.d'))
74
def get_conf_path(data_dir, config_name):
75
"""Return the path for an iSCSI config file.
77
:param data_dir: Base data directory. The config should be in its
78
`tgt.conf.d` subdirectory.
79
:param config_name: Base name for the config. A ".conf" suffix will be
82
return os.path.join(tgt_conf_d(data_dir), "%s.conf" % config_name)
85
def get_target_name(release, version, arch, version_name):
86
"""Compose a target's name based on its parameters.
88
The `**kwargs` are inert. They are only here for calling convenience.
90
return '-'.join(['maas', release, version, arch, version_name])
93
def tgt_admin_delete(name):
94
"""Delete a target using `tgt-admin`."""
95
call_and_check(TGT_ADMIN + ["--delete", prefix_target_name(name)])
98
class TargetNotCreated(RuntimeError):
99
"""tgt-admin failed to create a target."""
102
def target_exists(full_name):
103
"""Run `tgt --show` to determine whether the given target exists.
105
:param full_name: Full target name, including `ISCSI_TARGET_NAME_PREFIX`.
108
status = call_capture_and_check(TGT_ADMIN + ["--show"])
109
regex = b'^Target [0-9]+: %s\\s*$' % re.escape(full_name).encode('ascii')
110
match = re.search(regex, status, flags=re.MULTILINE)
111
return match is not None
114
def tgt_admin_update(target_dir, target_name):
115
"""Update a target using `tgt-admin`.
117
Actually we use this to add new targets.
119
full_name = prefix_target_name(target_name)
120
call_and_check(TGT_ADMIN + ["--update", full_name])
121
# Check that the target was really created.
122
# Reportedly tgt-admin tends to return 0 even when it fails, so check
124
if not target_exists(full_name):
125
raise TargetNotCreated("Failed tgt-admin add for %s" % full_name)
128
def set_up_data_dir(data_dir):
129
"""Create a data directory and its configuration directory."""
130
ensure_dir(tgt_conf_d(data_dir))
132
os.path.join(data_dir, 'tgt.conf'),
133
DATA_DIR_TGT_CONF_TEMPLATE.format(path=tgt_conf_d(data_dir)))
136
def write_info_file(target_dir, target_name, release, label, serial, arch):
137
"""Write the `info` file based on the given parameters."""
138
text = INFO_TEMPLATE.format(
139
release=release, label=label, serial=serial, arch=arch,
141
info_file = os.path.join(target_dir, 'info')
142
write_text_file(info_file, text)
145
def clean_up_info_file(target_dir):
146
"""To be called in the event of failure: move `info` file out of the way.
148
The `info` file will be renamed `info.failed`, in the same directory, and
149
any previously existing `info.failed` file is removed.
151
info_file = os.path.join(target_dir, 'info')
152
if not os.path.exists(info_file):
154
failed_file = info_file + '.failed'
155
if os.path.isfile(failed_file):
156
os.remove(failed_file)
157
shutil.move(info_file, failed_file)
160
def write_conf(path, target_name, image):
161
"""Write a `tgt.conf` file."""
162
text = TGT_CONF_TEMPLATE.format(target_name=target_name, image=image)
163
write_text_file(path, text)