33 |
33 |
# interpreted as representing official policies, either expressed
|
34 |
34 |
# or implied, of GRNET S.A.
|
35 |
35 |
|
|
36 |
"""
|
|
37 |
To add a command create a new class and add a 'command' decorator. The class
|
|
38 |
must have a 'main' method which will contain the code to be executed.
|
|
39 |
Optionally a command can implement an 'update_parser' class method in order
|
|
40 |
to add command line arguments, or modify the OptionParser in any way.
|
|
41 |
|
|
42 |
The name of the class is important and it will determine the name and grouping
|
|
43 |
of the command. This behavior can be overriden with the 'group' and 'name'
|
|
44 |
decorator arguments:
|
|
45 |
|
|
46 |
@command(api='nova')
|
|
47 |
class server_list(object):
|
|
48 |
# This command will be named 'list' under group 'server'
|
|
49 |
...
|
|
50 |
|
|
51 |
@command(api='nova', name='ls')
|
|
52 |
class server_list(object):
|
|
53 |
# This command will be named 'ls' under group 'server'
|
|
54 |
...
|
|
55 |
|
|
56 |
The docstring of a command class will be used as the command description in
|
|
57 |
help messages, unless overriden with the 'description' decorator argument.
|
|
58 |
|
|
59 |
The syntax of a command will be generated dynamically based on the signature
|
|
60 |
of the 'main' method, unless overriden with the 'syntax' decorator argument:
|
|
61 |
|
|
62 |
def main(self, server_id, network=None):
|
|
63 |
# This syntax of this command will be: '<server id> [network]'
|
|
64 |
...
|
|
65 |
|
|
66 |
The order of commands is important, it will be preserved in the help output.
|
|
67 |
"""
|
|
68 |
|
36 |
69 |
import inspect
|
37 |
70 |
import logging
|
38 |
71 |
import os
|
39 |
72 |
import sys
|
40 |
73 |
|
41 |
74 |
from base64 import b64encode
|
42 |
|
from collections import defaultdict
|
43 |
75 |
from grp import getgrgid
|
44 |
76 |
from optparse import OptionParser
|
45 |
77 |
from pwd import getpwuid
|
46 |
78 |
|
47 |
|
from client import Client, ClientError
|
|
79 |
from client import ComputeClient, ImagesClient, ClientError
|
|
80 |
from config import Config, ConfigError
|
|
81 |
from utils import OrderedDict, print_addresses, print_dict, print_items
|
48 |
82 |
|
49 |
83 |
|
50 |
|
API_ENV = 'KAMAKI_API'
|
51 |
|
URL_ENV = 'KAMAKI_URL'
|
52 |
|
TOKEN_ENV = 'KAMAKI_TOKEN'
|
53 |
|
RCFILE = '.kamakirc'
|
|
84 |
# Path to the file that stores the configuration
|
|
85 |
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
|
54 |
86 |
|
|
87 |
# Name of a shell variable to bypass the CONFIG_PATH value
|
|
88 |
CONFIG_ENV = 'KAMAKI_CONFIG'
|
|
89 |
|
|
90 |
# The defaults also determine the allowed keys
|
|
91 |
CONFIG_DEFAULTS = {
|
|
92 |
'apis': 'nova synnefo glance plankton',
|
|
93 |
'token': '',
|
|
94 |
'compute_url': 'https://okeanos.grnet.gr/api/v1',
|
|
95 |
'images_url': 'https://okeanos.grnet.gr/plankton',
|
|
96 |
}
|
55 |
97 |
|
56 |
98 |
log = logging.getLogger('kamaki')
|
57 |
99 |
|
|
100 |
_commands = OrderedDict()
|
|
101 |
|
58 |
102 |
|
59 |
|
def print_addresses(addresses, margin):
|
60 |
|
for address in addresses:
|
61 |
|
if address['id'] == 'public':
|
62 |
|
net = 'public'
|
63 |
|
else:
|
64 |
|
net = '%s/%s' % (address['id'], address['name'])
|
65 |
|
print '%s:' % net.rjust(margin + 4)
|
|
103 |
def command(api=None, group=None, name=None, description=None, syntax=None):
|
|
104 |
"""Class decorator that registers a class as a CLI command."""
|
|
105 |
|
|
106 |
def decorator(cls):
|
|
107 |
grp, sep, cmd = cls.__name__.partition('_')
|
|
108 |
if not sep:
|
|
109 |
grp, cmd = None, cls.__name__
|
66 |
110 |
|
67 |
|
ether = address.get('mac', None)
|
68 |
|
if ether:
|
69 |
|
print '%s: %s' % ('ether'.rjust(margin + 8), ether)
|
|
111 |
cls.api = api
|
|
112 |
cls.group = group or grp
|
|
113 |
cls.name = name or cmd
|
|
114 |
cls.description = description or cls.__doc__
|
|
115 |
cls.syntax = syntax
|
70 |
116 |
|
71 |
|
firewall = address.get('firewallProfile', None)
|
72 |
|
if firewall:
|
73 |
|
print '%s: %s' % ('firewall'.rjust(margin + 8), firewall)
|
|
117 |
if cls.syntax is None:
|
|
118 |
# Generate a syntax string based on main's arguments
|
|
119 |
spec = inspect.getargspec(cls.main.im_func)
|
|
120 |
args = spec.args[1:]
|
|
121 |
n = len(args) - len(spec.defaults or ())
|
|
122 |
required = ' '.join('<%s>' % x.replace('_', ' ') for x in args[:n])
|
|
123 |
optional = ' '.join('[%s]' % x.replace('_', ' ') for x in args[n:])
|
|
124 |
cls.syntax = ' '.join(x for x in [required, optional] if x)
|
74 |
125 |
|
75 |
|
for ip in address.get('values', []):
|
76 |
|
key = 'inet' if ip['version'] == 4 else 'inet6'
|
77 |
|
print '%s: %s' % (key.rjust(margin + 8), ip['addr'])
|
|
126 |
if cls.group not in _commands:
|
|
127 |
_commands[cls.group] = OrderedDict()
|
|
128 |
_commands[cls.group][cls.name] = cls
|
|
129 |
return cls
|
|
130 |
return decorator
|
78 |
131 |
|
79 |
132 |
|
80 |
|
def print_metadata(metadata, margin):
|
81 |
|
print '%s:' % 'metadata'.rjust(margin)
|
82 |
|
for key, val in metadata.get('values', {}).items():
|
83 |
|
print '%s: %s' % (key.rjust(margin + 4), val)
|
|
133 |
@command()
|
|
134 |
class config_list(object):
|
|
135 |
"""list configuration options"""
|
|
136 |
|
|
137 |
def main(self):
|
|
138 |
for key, val in sorted(self.config.items()):
|
|
139 |
print '%s=%s' % (key, val)
|
84 |
140 |
|
85 |
141 |
|
86 |
|
def print_dict(d, exclude=()):
|
87 |
|
if not d:
|
88 |
|
return
|
89 |
|
margin = max(len(key) for key in d) + 1
|
|
142 |
@command()
|
|
143 |
class config_get(object):
|
|
144 |
"""get a configuration option"""
|
90 |
145 |
|
91 |
|
for key, val in sorted(d.items()):
|
92 |
|
if key in exclude:
|
93 |
|
continue
|
94 |
|
|
95 |
|
if key == 'addresses':
|
96 |
|
print '%s:' % 'addresses'.rjust(margin)
|
97 |
|
print_addresses(val.get('values', []), margin)
|
98 |
|
continue
|
99 |
|
elif key == 'metadata':
|
100 |
|
print_metadata(val, margin)
|
101 |
|
continue
|
102 |
|
elif key == 'servers':
|
103 |
|
val = ', '.join(str(x) for x in val['values'])
|
104 |
|
|
105 |
|
print '%s: %s' % (key.rjust(margin), val)
|
106 |
|
|
107 |
|
|
108 |
|
def print_items(items, detail=False):
|
109 |
|
for item in items:
|
110 |
|
print '%s %s' % (item['id'], item.get('name', ''))
|
111 |
|
if detail:
|
112 |
|
print_dict(item, exclude=('id', 'name'))
|
113 |
|
print
|
114 |
|
|
115 |
|
|
116 |
|
class Command(object):
|
117 |
|
"""Abstract class.
|
118 |
|
|
119 |
|
All commands should subclass this class.
|
120 |
|
"""
|
121 |
|
|
122 |
|
api = 'openstack'
|
123 |
|
group = '<group>'
|
124 |
|
name = '<command>'
|
125 |
|
syntax = ''
|
126 |
|
description = ''
|
127 |
|
|
128 |
|
def __init__(self, argv):
|
129 |
|
self._init_parser(argv)
|
130 |
|
self._init_logging()
|
131 |
|
self._init_conf()
|
132 |
|
if self.name != '<command>':
|
133 |
|
self.client = Client(self.url, self.token)
|
134 |
|
|
135 |
|
def _init_parser(self, argv):
|
136 |
|
parser = OptionParser()
|
137 |
|
parser.usage = '%%prog %s %s %s [options]' % (self.group, self.name,
|
138 |
|
self.syntax)
|
139 |
|
parser.add_option('--api', dest='api', metavar='API',
|
140 |
|
help='API can be either openstack or synnefo')
|
141 |
|
parser.add_option('--url', dest='url', metavar='URL',
|
142 |
|
help='API URL')
|
143 |
|
parser.add_option('--token', dest='token', metavar='TOKEN',
|
144 |
|
help='use token TOKEN')
|
145 |
|
parser.add_option('-v', action='store_true', dest='verbose',
|
146 |
|
default=False, help='use verbose output')
|
147 |
|
parser.add_option('-d', action='store_true', dest='debug',
|
148 |
|
default=False, help='use debug output')
|
149 |
|
|
150 |
|
self.add_options(parser)
|
151 |
|
|
152 |
|
options, args = parser.parse_args(argv)
|
153 |
|
|
154 |
|
# Add options to self
|
155 |
|
for opt in parser.option_list:
|
156 |
|
key = opt.dest
|
157 |
|
if key:
|
158 |
|
val = getattr(options, key)
|
159 |
|
setattr(self, key, val)
|
160 |
|
|
161 |
|
self.args = args
|
162 |
|
self.parser = parser
|
163 |
|
|
164 |
|
def _init_logging(self):
|
165 |
|
if self.debug:
|
166 |
|
log.setLevel(logging.DEBUG)
|
167 |
|
elif self.verbose:
|
168 |
|
log.setLevel(logging.INFO)
|
169 |
|
else:
|
170 |
|
log.setLevel(logging.WARNING)
|
171 |
|
|
172 |
|
def _init_conf(self):
|
173 |
|
if not self.api:
|
174 |
|
self.api = os.environ.get(API_ENV, None)
|
175 |
|
if not self.url:
|
176 |
|
self.url = os.environ.get(URL_ENV, None)
|
177 |
|
if not self.token:
|
178 |
|
self.token = os.environ.get(TOKEN_ENV, None)
|
179 |
|
|
180 |
|
path = os.path.join(os.path.expanduser('~'), RCFILE)
|
181 |
|
if not os.path.exists(path):
|
182 |
|
return
|
183 |
|
|
184 |
|
for line in open(path):
|
185 |
|
key, sep, val = line.partition('=')
|
186 |
|
if not sep:
|
187 |
|
continue
|
188 |
|
key = key.strip().lower()
|
189 |
|
val = val.strip()
|
190 |
|
if key == 'api' and not self.api:
|
191 |
|
self.api = val
|
192 |
|
elif key == 'url' and not self.url:
|
193 |
|
self.url = val
|
194 |
|
elif key == 'token' and not self.token:
|
195 |
|
self.token = val
|
196 |
|
|
197 |
|
def add_options(self, parser):
|
198 |
|
pass
|
199 |
|
|
200 |
|
def main(self, *args):
|
201 |
|
pass
|
202 |
|
|
203 |
|
def execute(self):
|
204 |
|
try:
|
205 |
|
self.main(*self.args)
|
206 |
|
except TypeError:
|
207 |
|
self.parser.print_help()
|
208 |
|
|
209 |
|
|
210 |
|
# Server Group
|
211 |
|
|
212 |
|
class ListServers(Command):
|
213 |
|
group = 'server'
|
214 |
|
name = 'list'
|
215 |
|
description = 'list servers'
|
216 |
|
|
217 |
|
def add_options(self, parser):
|
218 |
|
parser.add_option('-l', action='store_true', dest='detail',
|
219 |
|
default=False, help='show detailed output')
|
|
146 |
def main(self, key):
|
|
147 |
val = self.config.get(key)
|
|
148 |
if val is not None:
|
|
149 |
print val
|
|
150 |
|
|
151 |
|
|
152 |
@command()
|
|
153 |
class config_set(object):
|
|
154 |
"""set a configuration option"""
|
|
155 |
|
|
156 |
def main(self, key, val):
|
|
157 |
self.config.set(key, val)
|
|
158 |
|
|
159 |
|
|
160 |
@command()
|
|
161 |
class config_del(object):
|
|
162 |
"""delete a configuration option"""
|
|
163 |
|
|
164 |
def main(self, key):
|
|
165 |
self.config.delete(key)
|
|
166 |
|
|
167 |
|
|
168 |
@command(api='nova')
|
|
169 |
class server_list(object):
|
|
170 |
"""list servers"""
|
|
171 |
|
|
172 |
@classmethod
|
|
173 |
def update_parser(cls, parser):
|
|
174 |
parser.add_option('-l', dest='detail', action='store_true',
|
|
175 |
default=False, help='show detailed output')
|
220 |
176 |
|
221 |
177 |
def main(self):
|
222 |
|
servers = self.client.list_servers(self.detail)
|
223 |
|
print_items(servers, self.detail)
|
|
178 |
servers = self.client.list_servers(self.options.detail)
|
|
179 |
print_items(servers, self.options.detail)
|
224 |
180 |
|
225 |
181 |
|
226 |
|
class GetServerDetails(Command):
|
227 |
|
group = 'server'
|
228 |
|
name = 'info'
|
229 |
|
syntax = '<server id>'
|
230 |
|
description = 'get server details'
|
|
182 |
@command(api='nova')
|
|
183 |
class server_info(object):
|
|
184 |
"""get server details"""
|
231 |
185 |
|
232 |
186 |
def main(self, server_id):
|
233 |
187 |
server = self.client.get_server_details(int(server_id))
|
234 |
188 |
print_dict(server)
|
235 |
189 |
|
236 |
190 |
|
237 |
|
class CreateServer(Command):
|
238 |
|
group = 'server'
|
239 |
|
name = 'create'
|
240 |
|
syntax = '<server name>'
|
241 |
|
description = 'create server'
|
242 |
|
|
243 |
|
def add_options(self, parser):
|
244 |
|
parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
|
245 |
|
help='use flavor FLAVOR_ID')
|
246 |
|
parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
|
247 |
|
help='use image IMAGE_ID')
|
248 |
|
parser.add_option('--personality',
|
249 |
|
dest='personalities',
|
250 |
|
action='append',
|
251 |
|
default=[],
|
252 |
|
metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
|
253 |
|
help='add a personality file')
|
|
191 |
@command(api='nova')
|
|
192 |
class server_create(object):
|
|
193 |
"""create server"""
|
254 |
194 |
|
255 |
|
def main(self, name):
|
256 |
|
flavor_id = int(self.flavor)
|
257 |
|
image_id = int(self.image)
|
|
195 |
@classmethod
|
|
196 |
def update_parser(cls, parser):
|
|
197 |
parser.add_option('--personality', dest='personalities',
|
|
198 |
action='append', default=[],
|
|
199 |
metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
|
|
200 |
help='add a personality file')
|
|
201 |
parser.epilog = "If missing, optional personality values will be " \
|
|
202 |
"filled based on the file at PATH if missing."
|
|
203 |
|
|
204 |
def main(self, name, flavor_id, image_id):
|
258 |
205 |
personalities = []
|
259 |
|
for personality in self.personalities:
|
|
206 |
for personality in self.options.personalities:
|
260 |
207 |
p = personality.split(',')
|
261 |
208 |
p.extend([None] * (5 - len(p))) # Fill missing fields with None
|
262 |
209 |
|
... | ... | |
264 |
211 |
|
265 |
212 |
if not path:
|
266 |
213 |
log.error("Invalid personality argument '%s'", p)
|
267 |
|
return
|
|
214 |
return 1
|
268 |
215 |
if not os.path.exists(path):
|
269 |
216 |
log.error("File %s does not exist", path)
|
270 |
|
return
|
|
217 |
return 1
|
271 |
218 |
|
272 |
219 |
with open(path) as f:
|
273 |
220 |
contents = b64encode(f.read())
|
... | ... | |
280 |
227 |
'mode': int(p[4]) if p[4] else 0x7777 & st.st_mode,
|
281 |
228 |
'contents': contents})
|
282 |
229 |
|
283 |
|
reply = self.client.create_server(name, flavor_id, image_id,
|
284 |
|
personalities)
|
|
230 |
reply = self.client.create_server(name, int(flavor_id), image_id,
|
|
231 |
personalities)
|
285 |
232 |
print_dict(reply)
|
286 |
233 |
|
287 |
234 |
|
288 |
|
class UpdateServerName(Command):
|
289 |
|
group = 'server'
|
290 |
|
name = 'rename'
|
291 |
|
syntax = '<server id> <new name>'
|
292 |
|
description = 'update server name'
|
|
235 |
@command(api='nova')
|
|
236 |
class server_rename(object):
|
|
237 |
"""update server name"""
|
293 |
238 |
|
294 |
239 |
def main(self, server_id, new_name):
|
295 |
240 |
self.client.update_server_name(int(server_id), new_name)
|
296 |
241 |
|
297 |
242 |
|
298 |
|
class DeleteServer(Command):
|
299 |
|
group = 'server'
|
300 |
|
name = 'delete'
|
301 |
|
syntax = '<server id>'
|
302 |
|
description = 'delete server'
|
|
243 |
@command(api='nova')
|
|
244 |
class server_delete(object):
|
|
245 |
"""delete server"""
|
303 |
246 |
|
304 |
247 |
def main(self, server_id):
|
305 |
248 |
self.client.delete_server(int(server_id))
|
306 |
249 |
|
307 |
250 |
|
308 |
|
class RebootServer(Command):
|
309 |
|
group = 'server'
|
310 |
|
name = 'reboot'
|
311 |
|
syntax = '<server id>'
|
312 |
|
description = 'reboot server'
|
|
251 |
@command(api='nova')
|
|
252 |
class server_reboot(object):
|
|
253 |
"""reboot server"""
|
313 |
254 |
|
314 |
|
def add_options(self, parser):
|
315 |
|
parser.add_option('-f', action='store_true', dest='hard',
|
316 |
|
default=False, help='perform a hard reboot')
|
|
255 |
@classmethod
|
|
256 |
def update_parser(cls, parser):
|
|
257 |
parser.add_option('-f', dest='hard', action='store_true',
|
|
258 |
default=False, help='perform a hard reboot')
|
317 |
259 |
|
318 |
260 |
def main(self, server_id):
|
319 |
|
self.client.reboot_server(int(server_id), self.hard)
|
|
261 |
self.client.reboot_server(int(server_id), self.options.hard)
|
320 |
262 |
|
321 |
263 |
|
322 |
|
class StartServer(Command):
|
323 |
|
api = 'synnefo'
|
324 |
|
group = 'server'
|
325 |
|
name = 'start'
|
326 |
|
syntax = '<server id>'
|
327 |
|
description = 'start server'
|
|
264 |
@command(api='synnefo')
|
|
265 |
class server_start(object):
|
|
266 |
"""start server"""
|
328 |
267 |
|
329 |
268 |
def main(self, server_id):
|
330 |
269 |
self.client.start_server(int(server_id))
|
331 |
270 |
|
332 |
271 |
|
333 |
|
class StartServer(Command):
|
334 |
|
api = 'synnefo'
|
335 |
|
group = 'server'
|
336 |
|
name = 'shutdown'
|
337 |
|
syntax = '<server id>'
|
338 |
|
description = 'shutdown server'
|
|
272 |
@command(api='synnefo')
|
|
273 |
class server_shutdown(object):
|
|
274 |
"""shutdown server"""
|
339 |
275 |
|
340 |
276 |
def main(self, server_id):
|
341 |
277 |
self.client.shutdown_server(int(server_id))
|
342 |
278 |
|
343 |
279 |
|
344 |
|
class ServerConsole(Command):
|
345 |
|
api = 'synnefo'
|
346 |
|
group = 'server'
|
347 |
|
name = 'console'
|
348 |
|
syntax = '<server id>'
|
349 |
|
description = 'get VNC console'
|
350 |
|
|
|
280 |
@command(api='synnefo')
|
|
281 |
class server_console(object):
|
|
282 |
"""get a VNC console"""
|
|
283 |
|
351 |
284 |
def main(self, server_id):
|
352 |
285 |
reply = self.client.get_server_console(int(server_id))
|
353 |
286 |
print_dict(reply)
|
354 |
287 |
|
355 |
288 |
|
356 |
|
class SetFirewallProfile(Command):
|
357 |
|
api = 'synnefo'
|
358 |
|
group = 'server'
|
359 |
|
name = 'firewall'
|
360 |
|
syntax = '<server id> <profile>'
|
361 |
|
description = 'set the firewall profile'
|
|
289 |
@command(api='synnefo')
|
|
290 |
class server_firewall(object):
|
|
291 |
"""set the firewall profile"""
|
362 |
292 |
|
363 |
293 |
def main(self, server_id, profile):
|
364 |
294 |
self.client.set_firewall_profile(int(server_id), profile)
|
365 |
295 |
|
366 |
296 |
|
367 |
|
class ListAddresses(Command):
|
368 |
|
group = 'server'
|
369 |
|
name = 'addr'
|
370 |
|
syntax = '<server id> [network]'
|
371 |
|
description = 'list server addresses'
|
|
297 |
@command(api='synnefo')
|
|
298 |
class server_addr(object):
|
|
299 |
"""list server addresses"""
|
372 |
300 |
|
373 |
301 |
def main(self, server_id, network=None):
|
374 |
302 |
reply = self.client.list_server_addresses(int(server_id), network)
|
... | ... | |
376 |
304 |
print_addresses(reply, margin)
|
377 |
305 |
|
378 |
306 |
|
379 |
|
class GetServerMeta(Command):
|
380 |
|
group = 'server'
|
381 |
|
name = 'meta'
|
382 |
|
syntax = '<server id> [key]'
|
383 |
|
description = 'get server metadata'
|
|
307 |
@command(api='nova')
|
|
308 |
class server_meta(object):
|
|
309 |
"""get server metadata"""
|
384 |
310 |
|
385 |
311 |
def main(self, server_id, key=None):
|
386 |
312 |
reply = self.client.get_server_metadata(int(server_id), key)
|
387 |
313 |
print_dict(reply)
|
388 |
314 |
|
389 |
315 |
|
390 |
|
class CreateServerMetadata(Command):
|
391 |
|
group = 'server'
|
392 |
|
name = 'addmeta'
|
393 |
|
syntax = '<server id> <key> <val>'
|
394 |
|
description = 'add server metadata'
|
|
316 |
@command(api='nova')
|
|
317 |
class server_addmeta(object):
|
|
318 |
"""add server metadata"""
|
395 |
319 |
|
396 |
320 |
def main(self, server_id, key, val):
|
397 |
321 |
reply = self.client.create_server_metadata(int(server_id), key, val)
|
398 |
322 |
print_dict(reply)
|
399 |
323 |
|
400 |
324 |
|
401 |
|
class UpdateServerMetadata(Command):
|
402 |
|
group = 'server'
|
403 |
|
name = 'setmeta'
|
404 |
|
syntax = '<server id> <key> <val>'
|
405 |
|
description = 'update server metadata'
|
|
325 |
@command(api='nova')
|
|
326 |
class server_setmeta(object):
|
|
327 |
"""update server metadata"""
|
406 |
328 |
|
407 |
329 |
def main(self, server_id, key, val):
|
408 |
330 |
metadata = {key: val}
|
... | ... | |
410 |
332 |
print_dict(reply)
|
411 |
333 |
|
412 |
334 |
|
413 |
|
class DeleteServerMetadata(Command):
|
414 |
|
group = 'server'
|
415 |
|
name = 'delmeta'
|
416 |
|
syntax = '<server id> <key>'
|
417 |
|
description = 'delete server metadata'
|
|
335 |
@command(api='nova')
|
|
336 |
class server_delmeta(object):
|
|
337 |
"""delete server metadata"""
|
418 |
338 |
|
419 |
339 |
def main(self, server_id, key):
|
420 |
340 |
self.client.delete_server_metadata(int(server_id), key)
|
421 |
341 |
|
422 |
342 |
|
423 |
|
class GetServerStats(Command):
|
424 |
|
api = 'synnefo'
|
425 |
|
group = 'server'
|
426 |
|
name = 'stats'
|
427 |
|
syntax = '<server id>'
|
428 |
|
description = 'get server statistics'
|
|
343 |
@command(api='synnefo')
|
|
344 |
class server_stats(object):
|
|
345 |
"""get server statistics"""
|
429 |
346 |
|
430 |
347 |
def main(self, server_id):
|
431 |
348 |
reply = self.client.get_server_stats(int(server_id))
|
432 |
349 |
print_dict(reply, exclude=('serverRef',))
|
433 |
350 |
|
434 |
351 |
|
435 |
|
# Flavor Group
|
436 |
|
|
437 |
|
class ListFlavors(Command):
|
438 |
|
group = 'flavor'
|
439 |
|
name = 'list'
|
440 |
|
description = 'list flavors'
|
|
352 |
@command(api='nova')
|
|
353 |
class flavor_list(object):
|
|
354 |
"""list flavors"""
|
|
355 |
|
|
356 |
@classmethod
|
|
357 |
def update_parser(cls, parser):
|
|
358 |
parser.add_option('-l', dest='detail', action='store_true',
|
|
359 |
default=False, help='show detailed output')
|
441 |
360 |
|
442 |
|
def add_options(self, parser):
|
443 |
|
parser.add_option('-l', action='store_true', dest='detail',
|
444 |
|
default=False, help='show detailed output')
|
445 |
|
|
446 |
361 |
def main(self):
|
447 |
|
flavors = self.client.list_flavors(self.detail)
|
448 |
|
print_items(flavors, self.detail)
|
|
362 |
flavors = self.client.list_flavors(self.options.detail)
|
|
363 |
print_items(flavors, self.options.detail)
|
449 |
364 |
|
450 |
365 |
|
451 |
|
class GetFlavorDetails(Command):
|
452 |
|
group = 'flavor'
|
453 |
|
name = 'info'
|
454 |
|
syntax = '<flavor id>'
|
455 |
|
description = 'get flavor details'
|
|
366 |
@command(api='nova')
|
|
367 |
class flavor_info(object):
|
|
368 |
"""get flavor details"""
|
456 |
369 |
|
457 |
370 |
def main(self, flavor_id):
|
458 |
371 |
flavor = self.client.get_flavor_details(int(flavor_id))
|
459 |
372 |
print_dict(flavor)
|
460 |
373 |
|
461 |
374 |
|
462 |
|
class ListImages(Command):
|
463 |
|
group = 'image'
|
464 |
|
name = 'list'
|
465 |
|
description = 'list images'
|
466 |
|
|
467 |
|
def add_options(self, parser):
|
468 |
|
parser.add_option('-l', action='store_true', dest='detail',
|
469 |
|
default=False, help='show detailed output')
|
470 |
|
|
|
375 |
@command(api='nova')
|
|
376 |
class image_list(object):
|
|
377 |
"""list images"""
|
|
378 |
|
|
379 |
@classmethod
|
|
380 |
def update_parser(cls, parser):
|
|
381 |
parser.add_option('-l', dest='detail', action='store_true',
|
|
382 |
default=False, help='show detailed output')
|
|
383 |
|
471 |
384 |
def main(self):
|
472 |
|
images = self.client.list_images(self.detail)
|
473 |
|
print_items(images, self.detail)
|
|
385 |
images = self.client.list_images(self.options.detail)
|
|
386 |
print_items(images, self.options.detail)
|
474 |
387 |
|
475 |
388 |
|
476 |
|
class GetImageDetails(Command):
|
477 |
|
group = 'image'
|
478 |
|
name = 'info'
|
479 |
|
syntax = '<image id>'
|
480 |
|
description = 'get image details'
|
|
389 |
@command(api='nova')
|
|
390 |
class image_info(object):
|
|
391 |
"""get image details"""
|
481 |
392 |
|
482 |
393 |
def main(self, image_id):
|
483 |
|
image = self.client.get_image_details(int(image_id))
|
|
394 |
image = self.client.get_image_details(image_id)
|
484 |
395 |
print_dict(image)
|
485 |
396 |
|
486 |
397 |
|
487 |
|
class CreateImage(Command):
|
488 |
|
group = 'image'
|
489 |
|
name = 'create'
|
490 |
|
syntax = '<server id> <image name>'
|
491 |
|
description = 'create image'
|
|
398 |
@command(api='nova')
|
|
399 |
class image_create(object):
|
|
400 |
"""create image"""
|
492 |
401 |
|
493 |
402 |
def main(self, server_id, name):
|
494 |
403 |
reply = self.client.create_image(int(server_id), name)
|
495 |
404 |
print_dict(reply)
|
496 |
405 |
|
497 |
406 |
|
498 |
|
class DeleteImage(Command):
|
499 |
|
group = 'image'
|
500 |
|
name = 'delete'
|
501 |
|
syntax = '<image id>'
|
502 |
|
description = 'delete image'
|
|
407 |
@command(api='nova')
|
|
408 |
class image_delete(object):
|
|
409 |
"""delete image"""
|
503 |
410 |
|
504 |
411 |
def main(self, image_id):
|
505 |
|
self.client.delete_image(int(image_id))
|
|
412 |
self.client.delete_image(image_id)
|
506 |
413 |
|
507 |
414 |
|
508 |
|
class GetImageMetadata(Command):
|
509 |
|
group = 'image'
|
510 |
|
name = 'meta'
|
511 |
|
syntax = '<image id> [key]'
|
512 |
|
description = 'get image metadata'
|
|
415 |
@command(api='nova')
|
|
416 |
class image_meta(object):
|
|
417 |
"""get image metadata"""
|
513 |
418 |
|
514 |
419 |
def main(self, image_id, key=None):
|
515 |
|
reply = self.client.get_image_metadata(int(image_id), key)
|
|
420 |
reply = self.client.get_image_metadata(image_id, key)
|
516 |
421 |
print_dict(reply)
|
517 |
422 |
|
518 |
423 |
|
519 |
|
class CreateImageMetadata(Command):
|
520 |
|
group = 'image'
|
521 |
|
name = 'addmeta'
|
522 |
|
syntax = '<image id> <key> <val>'
|
523 |
|
description = 'add image metadata'
|
|
424 |
@command(api='nova')
|
|
425 |
class image_addmeta(object):
|
|
426 |
"""add image metadata"""
|
524 |
427 |
|
525 |
428 |
def main(self, image_id, key, val):
|
526 |
|
reply = self.client.create_image_metadata(int(image_id), key, val)
|
|
429 |
reply = self.client.create_image_metadata(image_id, key, val)
|
527 |
430 |
print_dict(reply)
|
528 |
431 |
|
529 |
432 |
|
530 |
|
class UpdateImageMetadata(Command):
|
531 |
|
group = 'image'
|
532 |
|
name = 'setmeta'
|
533 |
|
syntax = '<image id> <key> <val>'
|
534 |
|
description = 'update image metadata'
|
|
433 |
@command(api='nova')
|
|
434 |
class image_setmeta(object):
|
|
435 |
"""update image metadata"""
|
535 |
436 |
|
536 |
437 |
def main(self, image_id, key, val):
|
537 |
438 |
metadata = {key: val}
|
538 |
|
reply = self.client.update_image_metadata(int(image_id), **metadata)
|
|
439 |
reply = self.client.update_image_metadata(image_id, **metadata)
|
539 |
440 |
print_dict(reply)
|
540 |
441 |
|
541 |
442 |
|
542 |
|
class DeleteImageMetadata(Command):
|
543 |
|
group = 'image'
|
544 |
|
name = 'delmeta'
|
545 |
|
syntax = '<image id> <key>'
|
546 |
|
description = 'delete image metadata'
|
|
443 |
@command(api='nova')
|
|
444 |
class image_delmeta(object):
|
|
445 |
"""delete image metadata"""
|
547 |
446 |
|
548 |
447 |
def main(self, image_id, key):
|
549 |
|
self.client.delete_image_metadata(int(image_id), key)
|
|
448 |
self.client.delete_image_metadata(image_id, key)
|
550 |
449 |
|
551 |
450 |
|
552 |
|
class ListNetworks(Command):
|
553 |
|
api = 'synnefo'
|
554 |
|
group = 'network'
|
555 |
|
name = 'list'
|
556 |
|
description = 'list networks'
|
|
451 |
@command(api='synnefo')
|
|
452 |
class network_list(object):
|
|
453 |
"""list networks"""
|
557 |
454 |
|
558 |
|
def add_options(self, parser):
|
559 |
|
parser.add_option('-l', action='store_true', dest='detail',
|
560 |
|
default=False, help='show detailed output')
|
|
455 |
@classmethod
|
|
456 |
def update_parser(cls, parser):
|
|
457 |
parser.add_option('-l', dest='detail', action='store_true',
|
|
458 |
default=False, help='show detailed output')
|
561 |
459 |
|
562 |
460 |
def main(self):
|
563 |
|
networks = self.client.list_networks(self.detail)
|
564 |
|
print_items(networks, self.detail)
|
|
461 |
networks = self.client.list_networks(self.options.detail)
|
|
462 |
print_items(networks, self.options.detail)
|
565 |
463 |
|
566 |
464 |
|
567 |
|
class CreateNetwork(Command):
|
568 |
|
api = 'synnefo'
|
569 |
|
group = 'network'
|
570 |
|
name = 'create'
|
571 |
|
syntax = '<network name>'
|
572 |
|
description = 'create a network'
|
|
465 |
@command(api='synnefo')
|
|
466 |
class network_create(object):
|
|
467 |
"""create a network"""
|
573 |
468 |
|
574 |
469 |
def main(self, name):
|
575 |
470 |
reply = self.client.create_network(name)
|
576 |
471 |
print_dict(reply)
|
577 |
472 |
|
578 |
473 |
|
579 |
|
class GetNetworkDetails(Command):
|
580 |
|
api = 'synnefo'
|
581 |
|
group = 'network'
|
582 |
|
name = 'info'
|
583 |
|
syntax = '<network id>'
|
584 |
|
description = 'get network details'
|
585 |
|
|
|
474 |
@command(api='synnefo')
|
|
475 |
class network_info(object):
|
|
476 |
"""get network details"""
|
|
477 |
|
586 |
478 |
def main(self, network_id):
|
587 |
479 |
network = self.client.get_network_details(network_id)
|
588 |
480 |
print_dict(network)
|
589 |
481 |
|
590 |
482 |
|
591 |
|
class RenameNetwork(Command):
|
592 |
|
api = 'synnefo'
|
593 |
|
group = 'network'
|
594 |
|
name = 'rename'
|
595 |
|
syntax = '<network id> <new name>'
|
596 |
|
description = 'update network name'
|
|
483 |
@command(api='synnefo')
|
|
484 |
class network_rename(object):
|
|
485 |
"""update network name"""
|
597 |
486 |
|
598 |
|
def main(self, network_id, name):
|
599 |
|
self.client.update_network_name(network_id, name)
|
|
487 |
def main(self, network_id, new_name):
|
|
488 |
self.client.update_network_name(network_id, new_name)
|
600 |
489 |
|
601 |
490 |
|
602 |
|
class DeleteNetwork(Command):
|
603 |
|
api = 'synnefo'
|
604 |
|
group = 'network'
|
605 |
|
name = 'delete'
|
606 |
|
syntax = '<network id>'
|
607 |
|
description = 'delete a network'
|
|
491 |
@command(api='synnefo')
|
|
492 |
class network_delete(object):
|
|
493 |
"""delete a network"""
|
608 |
494 |
|
609 |
495 |
def main(self, network_id):
|
610 |
496 |
self.client.delete_network(network_id)
|
611 |
497 |
|
612 |
|
class ConnectServer(Command):
|
613 |
|
api = 'synnefo'
|
614 |
|
group = 'network'
|
615 |
|
name = 'connect'
|
616 |
|
syntax = '<server id> <network id>'
|
617 |
|
description = 'connect a server to a network'
|
|
498 |
|
|
499 |
@command(api='synnefo')
|
|
500 |
class network_connect(object):
|
|
501 |
"""connect a server to a network"""
|
618 |
502 |
|
619 |
503 |
def main(self, server_id, network_id):
|
620 |
504 |
self.client.connect_server(server_id, network_id)
|
621 |
505 |
|
622 |
506 |
|
623 |
|
class DisconnectServer(Command):
|
624 |
|
api = 'synnefo'
|
625 |
|
group = 'network'
|
626 |
|
name = 'disconnect'
|
627 |
|
syntax = '<server id> <network id>'
|
628 |
|
description = 'disconnect a server from a network'
|
629 |
|
|
|
507 |
@command(api='synnefo')
|
|
508 |
class network_disconnect(object):
|
|
509 |
"""disconnect a server from a network"""
|
|
510 |
|
630 |
511 |
def main(self, server_id, network_id):
|
631 |
512 |
self.client.disconnect_server(server_id, network_id)
|
632 |
513 |
|
633 |
514 |
|
634 |
|
|
635 |
|
def print_usage(exe, groups, group=None):
|
636 |
|
nop = Command([])
|
637 |
|
nop.parser.print_help()
|
|
515 |
@command(api='glance')
|
|
516 |
class glance_list(object):
|
|
517 |
"""list images"""
|
638 |
518 |
|
|
519 |
def main(self):
|
|
520 |
images = self.client.list_public()
|
|
521 |
print images
|
|
522 |
|
|
523 |
|
|
524 |
def print_groups(groups):
|
|
525 |
print
|
|
526 |
print 'Groups:'
|
|
527 |
for group in groups:
|
|
528 |
print ' %s' % group
|
|
529 |
|
|
530 |
|
|
531 |
def print_commands(group, commands):
|
639 |
532 |
print
|
640 |
533 |
print 'Commands:'
|
641 |
|
|
642 |
|
if group:
|
643 |
|
items = [(group, groups[group])]
|
644 |
|
else:
|
645 |
|
items = sorted(groups.items())
|
646 |
|
|
647 |
|
for group, commands in items:
|
648 |
|
for command, cls in sorted(commands.items()):
|
649 |
|
name = ' %s %s' % (group, command)
|
650 |
|
print '%s %s' % (name.ljust(22), cls.description)
|
651 |
|
print
|
|
534 |
for name, cls in _commands[group].items():
|
|
535 |
if name in commands:
|
|
536 |
print ' %s %s' % (name.ljust(10), cls.description)
|
652 |
537 |
|
653 |
538 |
|
654 |
539 |
def main():
|
655 |
|
nop = Command([])
|
656 |
|
groups = defaultdict(dict)
|
657 |
|
module = sys.modules[__name__]
|
658 |
|
for name, cls in inspect.getmembers(module, inspect.isclass):
|
659 |
|
if issubclass(cls, Command) and cls != Command:
|
660 |
|
if nop.api == 'openstack' and nop.api != cls.api:
|
661 |
|
continue # Ignore synnefo commands
|
662 |
|
groups[cls.group][cls.name] = cls
|
|
540 |
parser = OptionParser(add_help_option=False)
|
|
541 |
parser.usage = '%prog <group> <command> [options]'
|
|
542 |
parser.add_option('--help', dest='help', action='store_true',
|
|
543 |
default=False, help='show this help message and exit')
|
|
544 |
parser.add_option('--api', dest='apis', metavar='API', action='append',
|
|
545 |
help='API to use (can be used multiple times)')
|
|
546 |
parser.add_option('--compute-url', dest='compute_url', metavar='URL',
|
|
547 |
help='URL for the compute API')
|
|
548 |
parser.add_option('--images-url', dest='images_url', metavar='URL',
|
|
549 |
help='URL for the images API')
|
|
550 |
parser.add_option('--token', dest='token', metavar='TOKEN',
|
|
551 |
help='use token TOKEN')
|
|
552 |
parser.add_option('-v', dest='verbose', action='store_true', default=False,
|
|
553 |
help='use verbose output')
|
|
554 |
parser.add_option('-d', dest='debug', action='store_true', default=False,
|
|
555 |
help='use debug output')
|
|
556 |
|
|
557 |
# Do a preliminary parsing, ignore any errors since we will print help
|
|
558 |
# anyway if we don't reach the main parsing.
|
|
559 |
_error = parser.error
|
|
560 |
parser.error = lambda msg: None
|
|
561 |
options, args = parser.parse_args(sys.argv)
|
|
562 |
parser.error = _error
|
|
563 |
|
|
564 |
if options.debug:
|
|
565 |
log.setLevel(logging.DEBUG)
|
|
566 |
elif options.verbose:
|
|
567 |
log.setLevel(logging.INFO)
|
|
568 |
else:
|
|
569 |
log.setLevel(logging.WARNING)
|
663 |
570 |
|
664 |
|
argv = list(sys.argv)
|
665 |
|
exe = os.path.basename(argv.pop(0))
|
666 |
|
group = argv.pop(0) if argv else None
|
667 |
|
command = argv.pop(0) if argv else None
|
|
571 |
try:
|
|
572 |
config = Config(CONFIG_PATH, CONFIG_ENV, CONFIG_DEFAULTS)
|
|
573 |
except ConfigError, e:
|
|
574 |
log.error('%s', e.args[0])
|
|
575 |
return 1
|
|
576 |
|
|
577 |
for key in CONFIG_DEFAULTS:
|
|
578 |
config.override(key, getattr(options, key))
|
|
579 |
|
|
580 |
apis = config.get('apis').split()
|
|
581 |
|
|
582 |
# Find available groups based on the given APIs
|
|
583 |
available_groups = []
|
|
584 |
for group, group_commands in _commands.items():
|
|
585 |
for name, cls in group_commands.items():
|
|
586 |
if cls.api is None or cls.api in apis:
|
|
587 |
available_groups.append(group)
|
|
588 |
break
|
|
589 |
|
|
590 |
if len(args) < 2:
|
|
591 |
parser.print_help()
|
|
592 |
print_groups(available_groups)
|
|
593 |
return 0
|
|
594 |
|
|
595 |
group = args[1]
|
|
596 |
|
|
597 |
if group not in available_groups:
|
|
598 |
parser.print_help()
|
|
599 |
print_groups(available_groups)
|
|
600 |
return 1
|
|
601 |
|
|
602 |
# Find available commands based on the given APIs
|
|
603 |
available_commands = []
|
|
604 |
for name, cls in _commands[group].items():
|
|
605 |
if cls.api is None or cls.api in apis:
|
|
606 |
available_commands.append(name)
|
|
607 |
continue
|
|
608 |
|
|
609 |
parser.usage = '%%prog %s <command> [options]' % group
|
|
610 |
|
|
611 |
if len(args) < 3:
|
|
612 |
parser.print_help()
|
|
613 |
print_commands(group, available_commands)
|
|
614 |
return 0
|
|
615 |
|
|
616 |
name = args[2]
|
668 |
617 |
|
669 |
|
if group not in groups:
|
670 |
|
group = None
|
|
618 |
if name not in available_commands:
|
|
619 |
parser.print_help()
|
|
620 |
print_commands(group, available_commands)
|
|
621 |
return 1
|
671 |
622 |
|
672 |
|
if not group or command not in groups[group]:
|
673 |
|
print_usage(exe, groups, group)
|
674 |
|
sys.exit(1)
|
|
623 |
cls = _commands[group][name]
|
|
624 |
cls.config = config
|
675 |
625 |
|
676 |
|
cls = groups[group][command]
|
|
626 |
syntax = '%s [options]' % cls.syntax if cls.syntax else '[options]'
|
|
627 |
parser.usage = '%%prog %s %s %s' % (group, name, syntax)
|
|
628 |
parser.epilog = ''
|
|
629 |
if hasattr(cls, 'update_parser'):
|
|
630 |
cls.update_parser(parser)
|
|
631 |
|
|
632 |
options, args = parser.parse_args(sys.argv)
|
|
633 |
if options.help:
|
|
634 |
parser.print_help()
|
|
635 |
return 0
|
|
636 |
|
|
637 |
cmd = cls()
|
|
638 |
cmd.config = config
|
|
639 |
cmd.options = options
|
|
640 |
|
|
641 |
if cmd.api in ('nova', 'synnefo'):
|
|
642 |
url = config.get('compute_url')
|
|
643 |
token = config.get('token')
|
|
644 |
cmd.client = ComputeClient(url, token)
|
|
645 |
elif cmd.api in ('glance', 'plankton'):
|
|
646 |
url = config.get('images_url')
|
|
647 |
token = config.get('token')
|
|
648 |
cmd.client = ImagesClient(url, token)
|
677 |
649 |
|
678 |
650 |
try:
|
679 |
|
cmd = cls(argv)
|
680 |
|
cmd.execute()
|
|
651 |
return cmd.main(*args[3:])
|
|
652 |
except TypeError:
|
|
653 |
parser.print_help()
|
|
654 |
return 1
|
681 |
655 |
except ClientError, err:
|
682 |
656 |
log.error('%s', err.message)
|
683 |
657 |
log.info('%s', err.details)
|
|
658 |
return 2
|
684 |
659 |
|
685 |
660 |
|
686 |
661 |
if __name__ == '__main__':
|
687 |
662 |
ch = logging.StreamHandler()
|
688 |
663 |
ch.setFormatter(logging.Formatter('%(message)s'))
|
689 |
664 |
log.addHandler(ch)
|
690 |
|
|
691 |
|
main()
|
|
665 |
err = main() or 0
|
|
666 |
sys.exit(err)
|