CLICommand.java 20.7 KB
Newer Older
1 2 3
/*
 * The MIT License
 *
4
 * Copyright (c) 2004-2010, Sun Microsystems, Inc.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.cli;

26
import hudson.AbortException;
27 28
import hudson.Extension;
import hudson.ExtensionList;
29 30 31
import hudson.ExtensionPoint;
import hudson.cli.declarative.CLIMethod;
import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
32
import hudson.cli.declarative.OptionHandlerExtension;
33
import jenkins.model.Jenkins;
34 35
import hudson.remoting.Callable;
import hudson.remoting.Channel;
36
import hudson.remoting.ChannelProperty;
37
import hudson.security.CliAuthenticator;
38
import hudson.security.SecurityRealm;
39
import org.acegisecurity.Authentication;
K
Kohsuke Kawaguchi 已提交
40
import org.acegisecurity.BadCredentialsException;
41 42
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
43 44 45 46 47
import org.apache.commons.discovery.ResourceClassIterator;
import org.apache.commons.discovery.ResourceNameIterator;
import org.apache.commons.discovery.resource.ClassLoaders;
import org.apache.commons.discovery.resource.classes.DiscoverClasses;
import org.apache.commons.discovery.resource.names.DiscoverServiceNames;
48 49
import org.jvnet.hudson.annotation_indexer.Index;
import org.jvnet.tiger_types.Types;
50 51
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
52
import org.kohsuke.args4j.ClassParser;
53
import org.kohsuke.args4j.CmdLineException;
54
import org.kohsuke.args4j.CmdLineParser;
55
import org.kohsuke.args4j.spi.OptionHandler;
56

K
kohsuke 已提交
57
import java.io.BufferedInputStream;
58
import java.io.ByteArrayOutputStream;
59
import java.io.IOException;
60 61
import java.io.InputStream;
import java.io.PrintStream;
62
import java.lang.reflect.Type;
63 64
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
65
import java.util.List;
K
kohsuke 已提交
66
import java.util.Locale;
K
Kohsuke Kawaguchi 已提交
67
import java.util.UUID;
68
import java.util.logging.Level;
69
import java.util.logging.Logger;
70 71 72 73

/**
 * Base class for Hudson CLI.
 *
K
kohsuke 已提交
74
 * <h2>How does a CLI command work</h2>
75
 * <p>
K
kohsuke 已提交
76
 * The users starts {@linkplain CLI the "CLI agent"} on a remote system, by specifying arguments, like
77
 * <tt>"java -jar jenkins-cli.jar command arg1 arg2 arg3"</tt>. The CLI agent creates
K
kohsuke 已提交
78 79
 * a remoting channel with the server, and it sends the entire arguments to the server, along with
 * the remoted stdin/out/err.
80 81
 *
 * <p>
K
kohsuke 已提交
82
 * The Hudson master then picks the right {@link CLICommand} to execute, clone it, and
83
 * calls {@link #main(List, Locale, InputStream, PrintStream, PrintStream)} method.
K
kohsuke 已提交
84 85
 *
 * <h2>Note for CLI command implementor</h2>
K
Kohsuke Kawaguchi 已提交
86
 * Start with <a href="http://wiki.jenkins-ci.org/display/JENKINS/Writing+CLI+commands">this document</a>
K
kohsuke 已提交
87 88
 * to get the general idea of CLI.
 *
K
kohsuke 已提交
89 90 91 92 93
 * <ul>
 * <li>
 * Put {@link Extension} on your implementation to have it discovered by Hudson.
 *
 * <li>
J
Jesse Glick 已提交
94
 * Use <a href="https://github.com/kohsuke/args4j">args4j</a> annotation on your implementation to define
K
kohsuke 已提交
95
 * options and arguments (however, if you don't like that, you could override
96
 * the {@link #main(List, Locale, InputStream, PrintStream, PrintStream)} method directly.
97
 *
K
kohsuke 已提交
98 99 100 101 102 103 104 105
 * <li>
 * stdin, stdout, stderr are remoted, so proper buffering is necessary for good user experience.
 *
 * <li>
 * Send {@link Callable} to a CLI agent by using {@link #channel} to get local interaction,
 * such as uploading a file, asking for a password, etc.
 *
 * </ul>
106 107 108
 *
 * @author Kohsuke Kawaguchi
 * @since 1.302
109
 * @see CLIMethod
110
 */
111
@LegacyInstancesAreScopedToHudson
112 113 114 115 116 117
public abstract class CLICommand implements ExtensionPoint, Cloneable {
    /**
     * Connected to stdout and stderr of the CLI agent that initiated the session.
     * IOW, if you write to these streams, the person who launched the CLI command
     * will see the messages in his terminal.
     *
K
kohsuke 已提交
118
     * <p>
119 120 121
     * (In contrast, calling {@code System.out.println(...)} would print out
     * the message to the server log file, which is probably not what you want.
     */
122
    public transient PrintStream stdout,stderr;
123

K
kohsuke 已提交
124 125 126 127 128 129
    /**
     * Connected to stdin of the CLI agent.
     *
     * <p>
     * This input stream is buffered to hide the latency in the remoting.
     */
130
    public transient InputStream stdin;
K
kohsuke 已提交
131

132 133 134
    /**
     * {@link Channel} that represents the CLI JVM. You can use this to
     * execute {@link Callable} on the CLI JVM, among other things.
K
Kohsuke Kawaguchi 已提交
135 136 137 138
     *
     * <p>
     * Starting 1.445, CLI transports are not required to provide a channel
     * (think of sshd, telnet, etc), so in such a case this field is null.
139 140 141 142
     * 
     * <p>
     * See {@link #checkChannel()} to get a channel and throw an user-friendly
     * exception
143
     */
144
    public transient Channel channel;
145

146 147 148
    /**
     * The locale of the client. Messages should be formatted with this resource.
     */
149
    public transient Locale locale;
150

K
Kohsuke Kawaguchi 已提交
151 152 153 154 155
    /**
     * Set by the caller of the CLI system if the transport already provides
     * authentication. Due to the compatibility issue, we still allow the user
     * to use command line switches to authenticate as other users.
     */
156
    private transient Authentication transportAuth;
157 158 159 160 161 162 163 164 165 166 167 168 169 170

    /**
     * Gets the command name.
     *
     * <p>
     * For example, if the CLI is invoked as <tt>java -jar cli.jar foo arg1 arg2 arg4</tt>,
     * on the server side {@link CLICommand} that returns "foo" from {@link #getName()}
     * will be invoked.
     *
     * <p>
     * By default, this method creates "foo-bar-zot" from "FooBarZotCommand".
     */
    public String getName() {
        String name = getClass().getName();
171
        name = name.substring(name.lastIndexOf('.') + 1); // short name
172
        name = name.substring(name.lastIndexOf('$')+1);
173 174 175 176
        if(name.endsWith("Command"))
            name = name.substring(0,name.length()-7); // trim off the command

        // convert "FooBarZot" into "foo-bar-zot"
K
kohsuke 已提交
177 178
        // Locale is fixed so that "CreateInstance" always become "create-instance" no matter where this is run.
        return name.replaceAll("([a-z0-9])([A-Z])","$1-$2").toLowerCase(Locale.ENGLISH);
179 180
    }

K
kohsuke 已提交
181 182 183 184 185 186
    /**
     * Gets the quick summary of what this command does.
     * Used by the help command to generate the list of commands.
     */
    public abstract String getShortDescription();

K
Kohsuke Kawaguchi 已提交
187 188 189 190 191 192 193
    /**
     * Entry point to the CLI command.
     * 
     * <p>
     * The default implementation uses args4j to parse command line arguments and call {@link #run()},
     * but if that processing is undesirable, subtypes can directly override this method and leave {@link #run()}
     * to an empty method.
194 195
     * You would however then have to consider {@link CliAuthenticator} and {@link #getTransportAuthentication},
     * so this is not really recommended.
K
Kohsuke Kawaguchi 已提交
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
     * 
     * @param args
     *      Arguments to the sub command. For example, if the CLI is invoked like "java -jar cli.jar foo bar zot",
     *      then "foo" is the sub-command and the argument list is ["bar","zot"].
     * @param locale
     *      Locale of the client (which can be different from that of the server.) Good behaving command implementation
     *      would use this locale for formatting messages.
     * @param stdin
     *      Connected to the stdin of the CLI client.
     * @param stdout
     *      Connected to the stdout of the CLI client.
     * @param stderr
     *      Connected to the stderr of the CLI client.
     * @return
     *      Exit code from the command.
     */
212
    public int main(List<String> args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) {
K
kohsuke 已提交
213
        this.stdin = new BufferedInputStream(stdin);
214 215
        this.stdout = stdout;
        this.stderr = stderr;
216
        this.locale = locale;
217
        registerOptionHandlers();
218
        CmdLineParser p = getCmdLineParser();
219 220 221 222 223

        // add options from the authenticator
        SecurityContext sc = SecurityContextHolder.getContext();
        Authentication old = sc.getAuthentication();

224
        CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this);
225
        sc.setAuthentication(getTransportAuthentication());
226
        new ClassParser().parse(authenticator,p);
227

228 229
        try {
            p.parseArgument(args.toArray(new String[args.size()]));
230
            Authentication auth = authenticator.authenticate();
231
            if (auth==Jenkins.ANONYMOUS)
232 233
                auth = loadStoredAuthentication();
            sc.setAuthentication(auth); // run the CLI with the right credential
234
            if (!(this instanceof LoginCommand || this instanceof HelpCommand))
235
                Jenkins.getInstance().checkPermission(Jenkins.READ);
236 237 238 239 240
            return run();
        } catch (CmdLineException e) {
            stderr.println(e.getMessage());
            printUsage(stderr, p);
            return -1;
241 242 243 244
        } catch (AbortException e) {
            // signals an error without stack trace
            stderr.println(e.getMessage());
            return -1;
K
Kohsuke Kawaguchi 已提交
245 246 247 248 249 250 251
        } catch (BadCredentialsException e) {
            // to the caller, we can't reveal whether the user didn't exist or the password didn't match.
            // do that to the server log instead
            String id = UUID.randomUUID().toString();
            LOGGER.log(Level.INFO, "CLI login attempt failed: "+id, e);
            stderr.println("Bad Credentials. Search the server log for "+id+" for more details.");
            return -1;
252 253 254
        } catch (Exception e) {
            e.printStackTrace(stderr);
            return -1;
255 256
        } finally {
            sc.setAuthentication(old); // restore
257 258
        }
    }
259 260 261 262

    /**
     * Get parser for this command.
     *
263
     * Exposed to be overridden by {@link hudson.cli.declarative.CLIRegisterer}.
264
     * @since 1.538
265 266 267 268
     */
    protected CmdLineParser getCmdLineParser() {
        return new CmdLineParser(this);
    }
269 270 271 272 273 274
    
    public Channel checkChannel() throws AbortException {
        if (channel==null)
            throw new AbortException("This command can only run with Jenkins CLI. See https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+CLI");
        return channel;
    }
275

276
    /**
277 278
     * Loads the persisted authentication information from {@link ClientAuthenticationCache}
     * if the current transport provides {@link Channel}.
279 280 281
     */
    protected Authentication loadStoredAuthentication() throws InterruptedException {
        try {
282 283
            if (channel!=null)
                return new ClientAuthenticationCache(channel).get();
284 285 286 287
        } catch (IOException e) {
            stderr.println("Failed to access the stored credential");
            e.printStackTrace(stderr);  // recover
        }
288
        return Jenkins.ANONYMOUS;
289 290
    }

291 292 293 294 295 296 297 298 299 300 301 302 303 304
    /**
     * Determines if the user authentication is attempted through CLI before running this command.
     *
     * <p>
     * If your command doesn't require any authentication whatsoever, and if you don't even want to let the user
     * authenticate, then override this method to always return false &mdash; doing so will result in all the commands
     * running as anonymous user credential.
     *
     * <p>
     * Note that even if this method returns true, the user can still skip aut 
     *
     * @param auth
     *      Always non-null.
     *      If the underlying transport had already performed authentication, this object is something other than
305
     *      {@link jenkins.model.Jenkins#ANONYMOUS}.
306 307
     */
    protected boolean shouldPerformAuthentication(Authentication auth) {
308
        return auth== Jenkins.ANONYMOUS;
309 310
    }

311 312 313 314 315 316 317 318 319 320 321 322 323 324
    /**
     * Returns the identity of the client as determined at the CLI transport level.
     *
     * <p>
     * When the CLI connection to the server is tunneled over HTTP, that HTTP connection
     * can authenticate the client, just like any other HTTP connections to the server
     * can authenticate the client. This method returns that information, if one is available.
     * By generalizing it, this method returns the identity obtained at the transport-level authentication.
     *
     * <p>
     * For example, imagine if the current {@link SecurityRealm} is doing Kerberos authentication,
     * then this method can return a valid identity of the client.
     *
     * <p>
325
     * If the transport doesn't do authentication, this method returns {@link jenkins.model.Jenkins#ANONYMOUS}.
326 327
     */
    public Authentication getTransportAuthentication() {
328
        Authentication a = transportAuth; 
329
        if (a==null)    a = Jenkins.ANONYMOUS;
330 331 332
        return a;
    }

333 334 335 336
    public void setTransportAuth(Authentication transportAuth) {
        this.transportAuth = transportAuth;
    }

337 338
    /**
     * Executes the command, and return the exit code.
K
Kohsuke Kawaguchi 已提交
339 340 341 342
     * 
     * <p>
     * This is an internal contract between {@link CLICommand} and its subtype.
     * To execute CLI method from outside, use {@link #main(List, Locale, InputStream, PrintStream, PrintStream)}
343 344 345
     *
     * @return
     *      0 to indicate a success, otherwise an error code.
346 347 348 349 350 351
     * @throws AbortException
     *      If the processing should be aborted. Hudson will report the error message
     *      without stack trace, and then exits this command.
     * @throws Exception
     *      All the other exceptions cause the stack trace to be dumped, and then
     *      the command exits with an error code.
352
     */
353
    protected abstract int run() throws Exception;
354 355

    protected void printUsage(PrintStream stderr, CmdLineParser p) {
356 357 358
        stderr.print("java -jar jenkins-cli.jar " + getName());
        p.printSingleLineUsage(stderr);
        stderr.println();
359
        printUsageSummary(stderr);
360 361 362
        p.printUsage(stderr);
    }

363 364 365 366 367 368
    /**
     * Get single line summary as a string.
     */
    @Restricted(NoExternalUse.class)
    public final String getSingleLineSummary() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
369
        getCmdLineParser().printSingleLineUsage(out);
370 371 372 373 374 375 376 377 378
        return out.toString();
    }

    /**
     * Get usage as a string.
     */
    @Restricted(NoExternalUse.class)
    public final String getUsage() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
379
        getCmdLineParser().printUsage(out);
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
        return out.toString();
    }

    /**
     * Get long description as a string.
     */
    @Restricted(NoExternalUse.class)
    public final String getLongDescription() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(out);

        printUsageSummary(ps);
        ps.close();
        return out.toString();
    }

396 397 398 399 400 401 402 403 404
    /**
     * Called while producing usage. This is a good method to override
     * to render the general description of the command that goes beyond
     * a single-line summary. 
     */
    protected void printUsageSummary(PrintStream stderr) {
        stderr.println(getShortDescription());
    }

405 406 407 408
    /**
     * Convenience method for subtypes to obtain the system property of the client.
     */
    protected String getClientSystemProperty(String name) throws IOException, InterruptedException {
409
        return checkChannel().call(new GetSystemProperty(name));
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
    }

    private static final class GetSystemProperty implements Callable<String, IOException> {
        private final String name;

        private GetSystemProperty(String name) {
            this.name = name;
        }

        public String call() throws IOException {
            return System.getProperty(name);
        }

        private static final long serialVersionUID = 1L;
    }

426
    protected Charset getClientCharset() throws IOException, InterruptedException {
427 428 429 430 431
        if (channel==null)
            // for SSH, assume the platform default encoding
            // this is in-line with the standard SSH behavior
            return Charset.defaultCharset();

432
        String charsetName = checkChannel().call(new GetCharset());
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
        try {
            return Charset.forName(charsetName);
        } catch (UnsupportedCharsetException e) {
            LOGGER.log(Level.FINE,"Server doesn't have charset "+charsetName);
            return Charset.defaultCharset();
        }
    }

    private static final class GetCharset implements Callable<String, IOException> {
        public String call() throws IOException {
            return Charset.defaultCharset().name();
        }

        private static final long serialVersionUID = 1L;
    }

K
Kohsuke Kawaguchi 已提交
449 450 451 452
    /**
     * Convenience method for subtypes to obtain environment variables of the client.
     */
    protected String getClientEnvironmentVariable(String name) throws IOException, InterruptedException {
453
        return checkChannel().call(new GetEnvironmentVariable(name));
K
Kohsuke Kawaguchi 已提交
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
    }

    private static final class GetEnvironmentVariable implements Callable<String, IOException> {
        private final String name;

        private GetEnvironmentVariable(String name) {
            this.name = name;
        }

        public String call() throws IOException {
            return System.getenv(name);
        }

        private static final long serialVersionUID = 1L;
    }

470 471 472 473 474 475 476 477 478 479 480 481 482
    /**
     * Creates a clone to be used to execute a command.
     */
    protected CLICommand createClone() {
        try {
            return getClass().newInstance();
        } catch (IllegalAccessException e) {
            throw new AssertionError(e);
        } catch (InstantiationException e) {
            throw new AssertionError(e);
        }
    }

483 484 485 486 487
    /**
     * Auto-discovers {@link OptionHandler}s and add them to the given command line parser.
     */
    protected void registerOptionHandlers() {
        try {
488
            for (Class c : Index.list(OptionHandlerExtension.class, Jenkins.getInstance().pluginManager.uberClassLoader,Class.class)) {
489 490 491 492 493 494 495
                Type t = Types.getBaseClass(c, OptionHandler.class);
                CmdLineParser.registerHandler(Types.erasure(Types.getTypeArgument(t,0)), c);
            }
        } catch (IOException e) {
            throw new Error(e);
        }
    }
496

497 498 499 500
    /**
     * Returns all the registered {@link CLICommand}s.
     */
    public static ExtensionList<CLICommand> all() {
501
        return ExtensionList.lookup(CLICommand.class);
502 503 504 505 506 507
    }

    /**
     * Obtains a copy of the command for invocation.
     */
    public static CLICommand clone(String name) {
508 509 510
        for (CLICommand cmd : all())
            if(name.equals(cmd.getName()))
                return cmd.createClone();
511 512
        return null;
    }
513 514

    private static final Logger LOGGER = Logger.getLogger(CLICommand.class.getName());
J
jpederzolli 已提交
515

516 517 518 519 520
    /**
     * Key for {@link Channel#getProperty(Object)} that links to the {@link Authentication} object
     * which captures the identity of the client given by the transport layer.
     */
    public static final ChannelProperty<Authentication> TRANSPORT_AUTHENTICATION = new ChannelProperty<Authentication>(Authentication.class,"transportAuthentication");
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535

    private static final ThreadLocal<CLICommand> CURRENT_COMMAND = new ThreadLocal<CLICommand>();

    /*package*/ static CLICommand setCurrent(CLICommand cmd) {
        CLICommand old = getCurrent();
        CURRENT_COMMAND.set(cmd);
        return old;
    }

    /**
     * If the calling thread is in the middle of executing a CLI command, return it. Otherwise null.
     */
    public static CLICommand getCurrent() {
        return CURRENT_COMMAND.get();
    }
536 537 538 539

    static {
        // register option handlers that are defined
        ClassLoaders cls = new ClassLoaders();
K
Kohsuke Kawaguchi 已提交
540 541 542 543 544 545 546 547 548 549 550 551 552 553
        Jenkins j = Jenkins.getInstance();
        if (j!=null) {// only when running on the master
            cls.put(j.getPluginManager().uberClassLoader);

            ResourceNameIterator servicesIter =
                new DiscoverServiceNames(cls).findResourceNames(OptionHandler.class.getName());
            final ResourceClassIterator itr =
                new DiscoverClasses(cls).findResourceClasses(servicesIter);

            while(itr.hasNext()) {
                Class h = itr.nextResourceClass().loadClass();
                Class c = Types.erasure(Types.getTypeArgument(Types.getBaseClass(h, OptionHandler.class), 0));
                CmdLineParser.registerHandler(c,h);
            }
554 555
        }
    }
556
}