Skip to content

Commit 4bd0cce

Browse files
Merge branch 'refactoring-a-lot-v2'
Merge version 0.2.1 in master for release. This release contains a huge refactoring of the deployment core.
2 parents 1cd39b3 + 1e9c155 commit 4bd0cce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+7445
-4577
lines changed

README.md

Lines changed: 124 additions & 210 deletions
Large diffs are not rendered by default.

TODO

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
0. implement __dir__
2+
2. Add dummy text backend to telnet_auth.
3+
3. clean-up logging in standalone shell.
4+
5. write documentation (sphinx)
5+
6. correct exceptions.
6+
7. Take $TERM from the client terminal. (In case of a fuse-filesystem-system, we can easily have another $TERM for each connection.)
7+
8. test 'hosts' vs. 'host'
8+
10. test exceptions in action.
9+
12. we should have at least a role named 'host' in SimpleNode
10+
17. When moving to another thread, hostcontext should be aware of that. Guarantee thread safety.
11+
18. Implement walk-function
12+
20. implement filesystem in userspace mapping. (sftp, etc...)
13+
21. update README
14+
22. Rename get and put to get_file and put_file and wrap it around Host.open()
15+
24. Inspection of required_property gives NotImplementedError, show a nicer message.
16+
17+
25. Allow creation of Env() objects which disallow connection to hosts. By
18+
using this, it's possible to safely and quickly evaluate all Query objects.
19+
And Look for reverse relationsships. e.g. List all the queries that point
20+
to a certain node. "--list-references" command.
21+
22+
https://code.google.com/p/fusepy/source/browse/trunk/sftp.py

deployer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.1.11'
1+
__version__ = '0.2.1'

deployer/cli.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
import termcolor
1414
import termios
1515
import time
16+
import logging
1617

1718
from twisted.internet import fdesc
18-
from deployer.pty import select
19+
from deployer.pseudo_terminal import select
1920
from deployer.std import raw_mode
2021
from deployer.console import Console
2122

@@ -107,6 +108,8 @@ def handle(self, parts):
107108
h = self.root
108109
parts = parts[:]
109110

111+
logging.info('Handle command line action "%s"' % ' '.join(parts))
112+
110113
while h and parts:# and not h.is_leaf:
111114
try:
112115
h = h.get_subhandler(parts[0])
@@ -579,7 +582,7 @@ def read(self):
579582
else:
580583
c = self.stdin.read(1)
581584

582-
if c == '[': # (91)
585+
if c in ('[', 'O'): # (91, 68)
583586
c = self.stdin.read(1)
584587

585588
# Cursor to left
@@ -638,17 +641,6 @@ def read(self):
638641
elif c == 'F':
639642
self.end()
640643

641-
elif c == 'O':
642-
c = self.stdin.read(1)
643-
644-
# Home
645-
if c == 'H':
646-
self.home()
647-
648-
# End
649-
elif c == 'F':
650-
self.end()
651-
652644
# Insert character
653645
else:
654646
if self.vi_navigation:

deployer/client.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env python
2+
3+
from deployer import __version__
4+
from deployer.contrib.default_config import example_settings
5+
from deployer.run.socket_client import list_sessions
6+
from deployer.run.socket_client import start as start_client
7+
from deployer.run.socket_server import start as start_server
8+
from deployer.run.standalone_shell import start as start_standalone
9+
from deployer.run.telnet_server import start as start_telnet_server
10+
11+
import docopt
12+
import getopt
13+
import getpass
14+
import sys
15+
16+
__doc__ = \
17+
"""Usage:
18+
client.py run [-s | --single-threaded | --socket SOCKET] [--path PATH]
19+
[--non-interactive] [--log LOGFILE]
20+
[--] [ACTION PARAMETER...]
21+
client.py listen [--log LOGFILE] [--non-interactive] [--socket SOCKET]
22+
client.py connect (--socket SOCKET) [--path PATH] [--] [ACTION PARAMETER...]
23+
client.py telnet-server [--port PORT] [--log LOGFILE] [--non-interactive]
24+
client.py list-sessions
25+
client.py -h | --help
26+
client.py --version
27+
28+
Options:
29+
-h, --help : Display this help text.
30+
-s, --single-threaded : Single threaded mode.
31+
--path PATH : Start the shell at the node with this location.
32+
--non-interactive : If possible, run script with as few interactions as
33+
possible. This will always choose the default
34+
options when asked for questions.
35+
--log LOGFILE : Write logging info to this file. (For debugging.)
36+
--socket SOCKET : The path of the unix socket.
37+
--version : Show version information.
38+
"""
39+
40+
def start(root_service, name=sys.argv[0], extra_loggers=None):
41+
"""
42+
Client startup point.
43+
"""
44+
a = docopt.docopt(__doc__.replace('client.py', name), version=__version__)
45+
46+
interactive = not a['--non-interactive']
47+
action = a['ACTION']
48+
parameters = a['PARAMETER']
49+
path = a['--path'].split('.') if a['--path'] else None
50+
extra_loggers = extra_loggers or []
51+
52+
# Socket name variable
53+
# In case of integers, they map to /tmp/deployer.sock.username.X
54+
socket_name = a['--socket']
55+
56+
if socket_name is not None and socket_name.isdigit():
57+
socket_name = '/tmp/deployer.sock.%s.%s' % (getpass.getuser(), socket_name)
58+
59+
# List sessions
60+
if a['list-sessions']:
61+
list_sessions()
62+
63+
# Telnet server
64+
elif a['telnet-server']:
65+
port = int(a['PORT']) if a['PORT'] is not None else 23
66+
start_telnet_server(root_service, logfile=a['--log'], port=port,
67+
extra_loggers=extra_loggers)
68+
69+
# Socket server
70+
elif a['listen']:
71+
socket_name = start_server(root_service, daemonized=False,
72+
shutdown_on_last_disconnect=False,
73+
interactive=interactive, logfile=a['--log'], socket=a['--socket'],
74+
extra_loggers=extra_loggers)
75+
76+
# Connect to socket
77+
elif a['connect']:
78+
start_client(socket_name, path, action_name=action, parameters=parameters)
79+
80+
# Single threaded client
81+
elif a['run'] and a['--single-threaded']:
82+
start_standalone(root_service, interactive=interactive, cd_path=path,
83+
action_name=action, parameters=parameters, logfile=a['--log'],
84+
extra_loggers=extra_loggers)
85+
86+
# Multithreaded
87+
elif a['run']:
88+
# If no socket has been given. Start a daemonized server in the
89+
# background, and use that socket instead.
90+
if not socket_name:
91+
socket_name = start_server(root_service, daemonized=True,
92+
shutdown_on_last_disconnect=True, interactive=interactive,
93+
logfile=a['--log'], extra_loggers=extra_loggers)
94+
95+
start_client(socket_name, path, action_name=action, parameters=parameters)
96+
97+
98+
if __name__ == '__main__':
99+
start(root_service=example_settings)

deployer/console.py

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,40 @@
44
import sys
55
import random
66

7+
__doc__ = \
8+
"""
9+
The ``console`` object is an interface for user interaction from within a
10+
``Node``. Among the input methods are choice lists, plain text input and password
11+
input.
12+
13+
It has output methods that take the terminal size into account, like pagination
14+
and multi-column display. It takes care of the pseudo terminal underneat.
15+
16+
Example:
17+
18+
::
19+
20+
class MyNode(Node):
21+
def do_something(self):
22+
if self.console.confirm('Should we really do this?', default=True):
23+
# Do it...
24+
pass
25+
26+
.. note:: When the script runs in a shell that was started with the
27+
``--non-interactive`` option, the default options will always be chosen
28+
automatically.
29+
30+
"""
31+
32+
733
class NoInput(Exception):
834
pass
935

1036

1137
class Console(object):
38+
"""
39+
Interface for user interaction from within a ``Node``.
40+
"""
1241
def __init__(self, pty):
1342
self._pty = pty
1443

@@ -18,8 +47,12 @@ def is_interactive(self):
1847

1948
def input(self, label, is_password=False, answers=None, default=None):
2049
"""
21-
Ask for input. (like raw_input, but nice colored.)
22-
'answers' can be either None or a list of the accepted answers.
50+
Ask for plain text input. (Similar to raw_input.)
51+
52+
:param is_password: Show stars instead of the actual user input.
53+
:type is_password: bool
54+
:param answers: A list of the accepted answers or None.
55+
:param default: Default answer.
2356
"""
2457
def print_question():
2558
answers_str = (' [%s]' % (','.join(answers)) if answers else '')
@@ -84,7 +117,10 @@ def read_answer():
84117

85118
def choice(self, question, options, allow_random=False, default=None):
86119
"""
87-
`options`: (name, value) list
120+
:param options: List of (name, value) tuples.
121+
:type options: list
122+
:param allow_random: If ``True``, the default option becomes 'choose random'.
123+
:type allow_random: bool
88124
"""
89125
if len(options) == 0:
90126
raise NoInput('No options given.')
@@ -126,7 +162,8 @@ def choice(self, question, options, allow_random=False, default=None):
126162

127163
def confirm(self, question, default=None):
128164
"""
129-
Print this yes/no question, and return True when the user answers 'yes'.
165+
Print this yes/no question, and return ``True`` when the user answers
166+
'Yes'.
130167
"""
131168
answer = 'invalid'
132169

@@ -140,72 +177,85 @@ def confirm(self, question, default=None):
140177
return answer in ('yes', 'y')
141178

142179
#
143-
# Service selector
180+
# Node selector
144181
#
145182

146-
def select_service(self, root_service, prompt='Select service', filter=None):
183+
def select_node(self, root_node, prompt='Select a node', filter=None):
147184
"""
148-
Show autocompletion for service selection.
185+
Show autocompletion for node selection.
149186
"""
150187
from deployer.cli import ExitCLILoop, Handler, HandlerType, CLInterface
151188

152-
class ServiceHandler(Handler):
153-
def __init__(self, service):
154-
self.service = service
189+
class NodeHandler(Handler):
190+
def __init__(self, node):
191+
self.node = node
155192

156193
@property
157194
def is_leaf(self):
158-
return not filter or filter(self.service)
195+
return not filter or filter(self.node)
159196

160197
@property
161198
def handler_type(self):
162-
class ServiceType(HandlerType):
163-
color = self.service.get_group().color
164-
return ServiceType()
199+
class NodeType(HandlerType):
200+
color = self.node.get_group().color
201+
return NodeType()
165202

166203
def complete_subhandlers(self, part):
167-
for name, subservice in self.service.get_subservices():
204+
for name, subnode in self.node.get_subnodes():
168205
if name.startswith(part):
169-
yield name, ServiceHandler(subservice)
206+
yield name, NodeHandler(subnode)
170207

171208
def get_subhandler(self, name):
172-
if self.service.has_subservice(name):
173-
subservice = self.service.get_subservice(name)
174-
return ServiceHandler(subservice)
209+
if self.node.has_subnode(name):
210+
subnode = self.node.get_subnode(name)
211+
return NodeHandler(subnode)
175212

176213
def __call__(self, context):
177-
raise ExitCLILoop(self.service)
214+
raise ExitCLILoop(self.node)
178215

179-
root_handler = ServiceHandler(root_service)
216+
root_handler = NodeHandler(root_node)
180217

181218
class Shell(CLInterface):
182219
@property
183220
def prompt(self):
184221
return colored('\n%s > ' % prompt, 'cyan')
185222

186-
not_found_message = 'Service not found...'
187-
not_a_leaf_message = 'Not a valid service...'
223+
not_found_message = 'Node not found...'
224+
not_a_leaf_message = 'Not a valid node...'
188225

189-
service_result = Shell(self._pty, root_handler).cmdloop()
226+
node_result = Shell(self._pty, root_handler).cmdloop()
190227

191-
if not service_result:
228+
if not node_result:
192229
raise NoInput
193230

194-
return select_service_isolation(service_result)
231+
return select_node_isolation(node_result)
195232

196-
def select_service_isolation(self, service):
233+
def select_node_isolation(self, node):
197234
"""
198235
Ask for a host, from a list of hosts.
199236
"""
200-
if service._is_isolated:
201-
return service
202-
else:
203-
options = [ (i.name, i.service) for i in service.get_isolations() ]
237+
from deployer.inspection import Inspector
238+
from deployer.node import IsolationIdentifierType
239+
240+
# List isolations first. (This is a list of index/node tuples.)
241+
options = [
242+
(' '.join(index), node) for index, node in
243+
Inspector(node).iter_isolations(identifier_type=IsolationIdentifierType.HOSTS_SLUG)
244+
]
245+
246+
if len(options) > 1:
204247
return self.choice('Choose a host', options, allow_random=True)
248+
else:
249+
return options[0][1]
205250

206251
def lesspipe(self, line_iterator):
207252
"""
208-
Paginator for output.
253+
Paginator for output. This will print one page at a time. When the user
254+
presses a key, the next page is printed. ``Ctrl-c`` or ``q`` will quit
255+
the paginator.
256+
257+
:param line_iterator: A generator function that yields lines (without
258+
trailing newline)
209259
"""
210260
height = self._pty.get_size()[0] - 1
211261

@@ -242,8 +292,8 @@ def lesspipe(self, line_iterator):
242292

243293
def in_columns(self, item_iterator, margin_left=0):
244294
"""
245-
`item_iterator' should be an iterable, which yields either
246-
basestring, or (colored_item, length)
295+
:param item_iterator: An iterable, which yields either ``basestring``
296+
instances, or (colored_item, length) tuples.
247297
"""
248298
# Helper functions for extracting items from the iterator
249299
def get_length(item):

0 commit comments

Comments
 (0)